(공룡책) - 3

2. 프로세스 관리

3. 프로세스

목표

  • 프로세스의 여러 성분과 운영체제에서 이 성분들이 어떻게 생겨먹었는지, 어떻게 일정관리되는지
  • 운영체제에서 프로세스 생성 / 종료되는 법, 이러한 연산 수행하는 시스템 호출을 통한 프로그램 개발하는 법
  • 공유 메모리를 통한 프로세스 간 통신과 메세지 전달을 통한 프로세스 간 통신
  • 파이프와 POSIX 공유 메모리를 통해 프로세스 간 통신하는 프로그램 설계
  • 소켓과 원격 프로시저 호출을 통한 클라이언트-서버 간 통신
  • 리눅스 운영체제와 상호작용하는 커널 모듈 설계

3.1. 프로세스 개념

CPU의 모든 활동을 뭐라고 불러야? 초기 컴퓨터는 일 job을 실행하는 배치 시스템. 이후에 시간 공유 시스템에서는 사용자 프로그램 user program 혹은 작업 task을 실행. 워드, 웹브라우저, 운영체제 내부 기능 같이 보통 동시에 실행되는 놈들 프로세스 process라 부름.

요즘은 프로세스라 부르는 데 이라는 용어가 역사가 있는 용어이긴 함. 고로 가끔 이라는 용어도 사용할 것( 스케줄링 등).

3.1.1. 프로세스

프로세스는 실행 중인 프로그램. 프로세스의 현재 활동의 상태는 프로그램 카운터 program counter의 값과 프로세서의 레지스터들의 내용으로 표현. 프로세스의 메모리 레이아웃:

  • 텍스트 섹션 text section - 실행할 코드
  • 자료 섹션 data section - 전역 변수
  • 힙 섹션 heap section - 프로그램 실행 시 동적 할당하는 메모리
  • 스택 섹션 stack section - 함수 실행 때 사용할 임시 자료 저장소 (함수 매개변수, 반환 주소, 지역 변수 등)

텍스트와 자료 섹션의 크기는 고정. 각 함수 호출 때마다 함수 매개변수, 지역 변수, 반환 주소 등을 포함한 활성화 기록 activation record이 스택에 푸시됨; 제어가 함수에서 반환될 때 활성화 기록은 스택에서 팝됨. 힙도 동적 할당에 따라 크기 변함. 스택과 힙은 서로를 향해 커지만 서로 겹치지 않도록 운영체제가 보장해야.

프로그램 자체는 프로세스가 아님. 프로그램은 수동적인 존재. 마치 명령어의 목록을 내용물로 갖는 디스크 내의 파일(실행 파일 executable file이라고도 부름)임. 반대로 프로세스는 능동적인 존재. 프로그램 카운터가 다음에 실행할 명령어를 갖고 있고, 필요한 자원도 갖고 있음. 실행 파일이 메모리에 로딩될 때 프로그램은 프로세스가 됨. 더블 클릭을 하거나 커맨드라인에 실행 파일 이름 적는 식으로 메모리 로드.

같은 프로그램이 두 프로세스를 가질 수 있음. 이 경우에도 둘은 서로 다른 프로세스임.

프로세스 자체가 다른 코드를 위한 실행 환경이 될 수도 있음. JVM이 예시. 컴파일된 자바 프로그램 Program.class 실행한다 가정하면:

java Program

이라고 입력함. java 명령어가 JVM 실행한 다음, 가상 기계에서 Program이라는 자바 프로그램 실행하는 것.

C 프로그램의 메모리 레이아웃

  • 전역 자료 섹션은 (a) 초기화된 자료와 (b) 초기화 안 된 자료로 나뉨
  • main() 함수에 전달될 argcargv 매개변수를 위한 섹션이 따로 있음
GNU의 size 명령어로 이 섹션들의 크기를 알 수 있음. 위의 실행 파일의 이름이 memory라 가정:
text    data    bss    dec    hex    filename
1158    284     8      1450   5aa    memory
data는 초기화 안 된 자료, bss는 초기화된 자료(bss기호로 시작하는 블록 block started by symbol이라는 역사가 깊은 표현.) dec이랑 hex 값은 세 섹션의 총합을 각각 10진수와 16진수로 나타낸 것.

3.1.2. 프로세스 상태

프로세스사 실행되면 상태 state가 바뀜. 프로세스의 상태는 프로세스의 현재 활동에 영향을 받음. 프로세스의 상태들:

  • 생성 new. 프로세스가 생성 중.
  • 실행 running. 명령어가 실행 중.
  • 대기 waiting. 프로세스가 이벤트가 발생하기를 대기 중(I/O 완료나 신호 수신 등).
  • 준비 ready. 새로운 프로세서에 할당되기를 대기 중.
  • 종료 terminated. 프로세스가 실행 종료함.

이름은 운영체제마다 달라서 중요하지 않음. 물론 이 상태의 역할에 대응하는 건 모든 운영체제에 다 있음. 중요한 건 한 순간에 오로지 한 프로세스만이 실행 중임. 많은 프로세스는 준비 상태거나 대기 상태.

3.1.3. 프로세스 제어 블록

운영체제에서 각 프로세스는 프로세스 제어 블록 process control block (PCB)의 형태. 작업 제어 블록 task control block이라고도 부름. PCB의 구성:

  • 프로세스 상태 process state
  • 프로그램 카운터 program counter. 이 프로세스에서 다음에 실행할 명령어의 주소
  • CPU 레지스터 CPU registers. 컴퓨터 구조에 따라 레지스터의 수, 종류 다름. 누산기, 색인 레지스터, 스택 포인터, 범용 레지스터 등. 인터럽트 발생 시 프로그램 카운터와 마찬가지로 이 상태 정보도 저장되어야 나중에 프로세스 재실행되도록 재스케줄링되면 정상적으로 지속됨.
  • CPU 스케줄링 정보 CPU-scheduling information. 프로세스 우선도, 스케줄링 큐 포인터, 등 스케줄링 매개변수에 대한 정보.
  • 메모리 관리 정보 memory-management information. 기준 레지스터, 한계 레지스터, 페이지 테이블, 세그먼트 테이블의 값과 같은 정보. 운영체제의 메모리 구조에 따라 다름.
  • 어카운팅 정보 accounting information. 사용된 CPU와 실제 시간의 양, 시간 한계, 어카운트 수, 일 혹은 프로세스 수, 등의 정보.
  • 입출력 상태 정보 I/O status information. 프로세스에 할당된 입출력 장치의 목록, 열은 파일의 목록 등의 정보.

리눅스의 프로세스 표현

<include/linux/sched.h>에 있는 C 구조체 task_struct로 표현. 프로세스의 부모 parent는 자기를 생성한 프로세스. 형제자매 siblings는 부모가 같은 프로세스들.
long state;                 /* 프로세스 상태 */
struct sched_entity se;     /* 스케줄링 정보 */
struct task_struct* parent; /* 부모 프로세스 */
struct list_head children;  /* 자식 프로세스 */
struct files_struct* files; /* 열린 파일 목록 */
struct mm_struct* mm;       /* 주소 공간 */
모든 활성 프로세스는 task_struct의 이중 연결 리스트로 표현. 커널에 현재 시스템에서 실행 중인 프로세스인 current 포인터 있음. 커널을 수정한다고 가정하면, 상태의 경우 다음과 같이 수정:
current->state = new_state;

PCB는 프로세스를 시작 혹은 재시작하기 위한 모든 정보와 몇몇 어카운팅 자료를 위한 저장소.

3.1.4. 스레드

지금까지 프로세스는 오로지 단일 스레스 thread로 실행된다고 가정. 요즘 현대적인 운영체제는 프로세스에서 여러 스레드 실행 가능. 즉, 한 번에 여러 작업 가능. 특히 다중 코어 시스템에서 여러 스레드 병렬적으로 실행할 수 있으니 유용. 스레드를 지원하는 시스템에서는 PCB에 각 스레드별 정보를 포함하도록 확장.

3.2. 프로세스 스케줄링

다중 프로그래밍의 목적

  • 동시에 여러 프로세스 실행해서 CPU 최대 효율
  • 프로세스 간 재빠르게 CPU 코어 스위칭해서 시간 공유

이거 해주는게 프로세스 스케줄러 process scheduler. 얘가 코어에 현재 실행 가능한 프로세스를 할당해줌.

다중 프로그래밍 차수 degree of multiprogramming: 현재 메모리에 있는 프로세스의 개수

다중 프로그래밍의 목적 달성과 시간 공유 간의 균형 또한 고려해야함. 일반적으로 대부분의 프로세스는 I/O 제약 혹은 CPU 제약의 것. I/O 제약 프로세스 I/O-bound process란 연산보다 I/O 더 시간을 많이 보내는 놈, CPU 제약 프로세스 CPU-bound process는 반대로 I/O 요청이 거의 안 일어나고 연산에 집중하는 놈.

3.2.1. 큐 스케줄링

프로세스가 시스템에 들어오면 준비 상태 큐 ready queue에 저장. 보통 연결 리스트의 형태로 저장.

다른 큐도 있음. 프로세스가 CPU 코어에 할당되면 실행이 되다가 언젠간 종료되거나, 인터럽트 받거나, 대기를 함. 만약 프로세스가 디스크 같은 입출력 요청을 보내면 디바이스가 상대적으로 프로세서보다 느리니까 프로세스는 대기 상태로 갈 것. 그럼 대기 상태 큐 wait queue에 저장됨.

프로세스 스케줄링을 보통 표현할 때 큐 다이어그램 queueing diagram으로 표현. 아래 그림 참고. 그림에 보면 준비 상태 큐랑 몇 개의 대기 상태 큐가 있음. 동그라미가 큐를 처리할 자원, 화살표가 시스템 내에서 프로세스의 흐름.

초기에 준비 상태 큐에 새 프로세스를 넣음. 실행이 될 때까지, 혹은 지명 dispatch될 때까지 대기. CPU 코어에 할당되어 실행되면 다음 중 하나가 발생:

  • 입출력 요청하여 입출력 대기 상태 큐에 들어갈 수도
  • 새 자식 프로세스 생성하여 자식 종료 전까지 대기 상태 큐에서 대기
  • 인터럽트나 할당된 시간 지나서 코어에서 강제로 쫓겨나 준비 상태 큐로 돌아갈 수도

처음 두 가지 경우는 나중엔 대기 상태에서 다시 준비 상태로 돌아옴. 종료되어 모든 큐에서 삭제되고 PCB와 자원 전부 해제될 때까지 무한 반복임.

3.2.2. CPU 스케줄링

이처럼 프로세스는 여러 큐를 왔다 갔다하는데, 여기서 어떤 프로세스가 뭘 할 지를 정하는게 CPU 스케줄러 CPU scheduler. 입출력 제약 프로세스야 입출력 요청 기다리기 전까지는 겨우 몇 밀리세컨드 정도 실행되겠지만 CPU 제약 프로세스는 시간 더 필요할 수도 있어 중간에 강제로 뺄 수도 있음. 그래서 CPU 스케줄러는 최소한 100 밀리세컨드에 한 번은 돌아야 함. 실제로는 더 빨리 돌음.

몇몇 운영체제는 교체 swapping를 통해 중간 단계의 스케줄링 사용하기도. 핵심은 메모리에서 프로세스를 제거해서 다중 프로그래밍의 차수를 낮추는 것임. 나중에 메모리에 다시 올려서 멈췄던 곳에서부터 재개하면 되니까. 이걸 교체라 부르는 이유는 현재 상태 저장해두고 메모리에서 디스크로 교체 아웃될 수도 있고, 나중에 디스크에서 메모리로 교체로 들어올 수도 있으니까. 보통 메모리 없어서 해제해야할 때 사용.

3.2.3. 문맥 교환

인터럽트 발생 시 시스템은 CPU 코어에서 실행 중이던 프로세스의 현재 문맥 context를 저장. 프로세스의 PCB에 문맥이 있음. CPU 레지스터 값, 프로세스 상태, 메모리 관리 정보 등 있음. 보통 사용자 모드나 커널 모드에서 CPU 코어의 현재 상태의 상태 저장 state save을 하고 상태 복구 state restore로 연산 재개함.

CPU 코어의 프로세스를 다른 걸로 바꾸려면 현재 프로세스의 상태 저장하고 다른 프로세스를 상태 복구해주면 됨. 이게 문맥 교환 context switch. 문맥 교환에 걸리는 시간은 순수하게 오버헤드임. 교환할 때 다른 의미있는 작업을 하지 않으니까. 기계마다 메모리 속도, 복사해야 할 레지스터 수, 특수 명령어의 존재 등에 따라 걸리는 시간 다 다름.보통은 몇 마이크로초 걸림.

모바일 시스템에서 다중 작업

iOS는 초기엔 사용자 어플리케이션 다중 작업 지원 안 함. iOS 4부터 한정된 다중 작업 지원함. 모바일 장치에서 전면 foreground 어플리케이션이 현재 실행 중이고 화면에 보이는 어플리케이션. 배경 background 어플리케이션이 메모리에는 있지만 화면을 차지하지는 않는 놈. 나중에 하드웨어 성능 좋아지니까 iPad 태블릿에 동시에 두 전면 앱 실행하게 해주는 화면 분할 split-screen 지원.

안드로이드는 처음부터 다중 작업 지원. 어플리케이션이 배경으로 가도 뭔가를 해야한다면, 서비스 service라는 배경 프로세스를 위해 실행되는 독립적인 어플리케이션 성분을 반드시 사용해야 함.

3.3. 프로세스 연산

3.3.1. 프로세스 생성

프로세스의 생성은 트리 tree를 구성하게 됨.

대부분의 운영체제에서는 프로세스를 고유한 프로세스 식별자 process identifier (혹은 pid)를 통해 구분. 보통 정수임.

위 그림이 리눅스 운영체제의 일반적인 프로세스 트리. (여기서는 프로세스라는 용어를 광범위한 의미로 사용. 리눅스에서는 보통 작업 task이라고 부름.) systemd는 부팅되면 처음으로 실행되는 프로그램. logind는 시스템에 직접 로그인하는 클라이언트 관리. sshdssh (시큐어 셸)을 통해 시스템에 연결하려는 클라이언트 관리.

유닉스랑 리눅스에서는 ps 명령어로 프로세스 목록 얻을 수 있음:

ps -el

위에서는 현재 시스템의 활성 프로세스의 모든 정보를 출력함. (리눅스에선 pstree로 시스템의 모든 프로세스의 트리를 보여줌.)

자식 프로세스는 운영체제로부터 직접 자원을 얻을 수도 있고, 부모 프로세스의 자원 내에서 해결해야할 수도. 부모는 자원을 구분해서 자식에게 줄 수도 있고, 공유를 할 수도 있음. 자식이 부모의 자원 범위 내에 한정하게 되면 모든 프로세스가 과도하게 많은 자식 프로세스를 생성해 시스템 과부하를 걸리게 하는 것을 막게 해줌.

부모 프로세스는 자원 뿐만 아니라 초기 정보(입력)를 자식 프로세스에 줄 수도 있음.

프로세스가 새 프로세스를 생성하면 다음 둘 중 하나임:

  1. 부모가 계속해서 자식과 동시에 실행됨
  2. 자식의 일부, 혹은 전부가 종료될 때까지 부모는 대기함

이 새 프로세스에 대해 주소 공간에서 발생 가능한 두 가지 경우:

  1. 자식 프로세스는 부모 프로세스의 복제본 (부모와 동일한 프로그램과 자료를 가짐)
  2. 자식 프로세스 안에 새 프로그램이 있음

유닉스에서는 프로세스마다 프로세스 식별자로 구분. fork() 시스템 호출로 새 프로세스 생성. 원본 프로세스의 복제본 주소 공간으로 구성될 것임. 이를 통해 부모 프로세스가 자식 프로세스와 잘 통신이 됨. 부모랑 자식 둘 다 fork() 명령어 이후에 계속 돌아갈 건데, 새 (자식) 프로세스일 경우 fork()가 0을 반환하는데, 자식의 (0이 아닌) 프로세스 식별자가 부모에 반환됨.

fork() 시스템 호출 후 두 프로세스 중 한 명은 보통 exec() 시스템 호출로 프로세스의 메모리 공간을 새 프로그램으로 교체함. exec 시스템 호출이 호출되면 이진 파일을 메모리에 올리고(exec() 시스템 호출을 포함한 프로그램의 메모리 이미지를 없앰) 실행시킴. 이렇게 해서 두 프로세스가 독립적이면서 서로 통신 가능. exec() 새 프로그램으로 프로세스의 주소 공간과 중첩되기 때문에 오류가 발생하지 않는 이상 exec()이 제어를 돌려주지는 않는다.

#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>

int main()
{
    pid_t pid;
    
    /* 자식 프로세스 포크 */
    pid = fork();
    
    if (pid < 0) {	/* 오류 발생 */
    	fprintf(stderr, "Fork Failed");
        return 1;
    } else if (pid == 0) {	/* 자식 프로세스 */
    	execlp("/bin/ls", "ls", NULL);
    } else {	/* 부모 프로세스 */
    	/* 자식이 끝날 때까지 부모는 대기함 */
        wait(NULL);
        printf("Child Complete");
    }
    
    return 0;
}

두 개의 서로 다른, 동일한 프로그램 실행 중인 복제본. 자식 프로세스에서 pid의 값은 0, 부모는 0보다 큰 정수값(사실은 그게 자식의 pid). 자식 프로세스는 부모로부터 특권, 스케줄링 속성, 자원 등 부여 받음. 자식 프로세스는 execlp 시스템 호출(exec()의 한 종류)로 자신의 주소 공간에 유닉스 명령어 /bin/ls(경로 목록 필요할 때 사용)를 중첩시킴.

#include <stdio.h>
#include <windows.h>

int main(VOID)
{
    STARTUPINFO si;
    PROCESS_INFORMATION pi;
    
    /* 메모리 할당 */
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
    ZeroMemory(&pi, sizeof(pi));
    
    /* 자식 프로세스 생성 */
    if (!CreateProcess(NULL, /* 커맨드 라인 사용 */
    		       "C:\\WINDOWS\\system32\\mspaint.exe", /* 명령어 */
        		NULL, /* 프로세스 핸들 상속 X */
        		NULL, /* 스레드 핸들 상속 X */
        		FALSE, /* 핸들 상속 금지 */
        		0, /* 생성 플래그 없음 */
        		NULL, /* 부모의 환경 블록 사용 */
        		NULL. /* 부모의 현존하는 경로 사용 */
        		&si,
        		&pi)) {
        fprintf(stderr, "Create Process Failed");
        return -1;
    }
    
    /* 자식이 끝날 때까지 부모는 대기 */
    WaitForSingleObject(pi.hProcess, INFINITE);
    printf("Child Complete");
    
    /* 핸들 종료 */
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
}

중요한 건 CreateProcessmspaint.exe 어플리케이션을 불러온다는 거지, 나머지 정보는 궁금하면 Windows API에서 직접 확인하셈.

STARTUPINFO는 새 프로세스의 속성(창 크기, 생김새, 표준 입출력 파일에 대한 핸들 등)을 의미. PROCESS_INFORMATION은 새 프로세스과 새 프로세스의 스레드들에 대한 핸들과 식별자를 포함.

initsystemd 프로세스

전통적으로 유닉스 체제에서는 init(System V init이라고도 부름)이 모든 프로세스의 루트. 부팅 후 처음으로 실행되고, pid도 1임.

리눅스는 이 방법을 차용했는데, 최근엔 systemd로 바꿈. System V init보다 좀 더 유연하고 더 많은 서비스 제공 가능.

3.3.2. 프로세스 종료

실행할 거 다 실행하고 운영체제에게 exit() 시스템 호출을 통해 삭제를 요청하면 프로세스는 종료됨. 이때 대기 중인 부모 프로세스에 (보통 정수로된) 상태 값을 반환할 수도 있음. 모든 프로세스의 자원은 해제되어 운영체제에게 반환됨.

적합한 시스템 호출(윈도우즈의 경우 TerminateProcess())에 의해 다른 프로세스에 의해 종료될 수도 있음. 보통은 부모만이 호출함. 다른 프로세스가 지 맘대로 죽여버릴 수도 있음. 부모가 자식의 식별자 등을 알아야 종료시킬 수 있음. 그래서 어떤 프로세스가 새 프로세스를 생성하면 생성된 프로세스에 대한 정보가 부모에게 전달됨.

부모가 자식을 종료시키는 경우는 여러 이유가 있음:

  • 자식이 자신에게 할당받은 자원을 초과해서 사용한 경우 (이게 일어났는지 아려면 자식의 상태를 확인하는 메커니즘이 반드시 있어야 함)
  • 자식에게 할당된 작업이 더 이상 필요하지 않음
  • 부모가 종료될 때 운영체제가 부모 없는 자식을 허용하지 않을 때

마지막 경우를 순차적 종료 cascading termination라 부르고, 보통 운영체제가 처리함.

리눅스랑 유닉스에서는 exit() 시스템 호출에 종료 상태를 매개변수로 주어 프로세스 종료.

/* 상태 1로 종료 */
exit(1);

정상 종료 상황에서는 exit()가 C 실시간 라이브러리(유닉스 실행 파일 중에 존재)에 기본적으로 있을테니 직간접적으로 호출 됨.

wait() 시스템 호출은 부모가 자식의 종료 상태를 얻을 수 있게 매개변수로 전달해줌.

pid_t pid;
int status;

pid = wait(&status);

프로세스 종료되면 자원은 다 해제되는데, 부모가 wait() 호출할 때까지 프로세스 표에 기록은 남아있음. 그래야 프로세스의 종료 상태를 알 수 있으니까. 프로세스가 종료됐는데 부모가 wait()을 호출 안 한 것을 좀비 zombie 프로세스라 부름. 모든 프로세스는 좀비가 될테지만 겨우 잠깐 뿐임. wait() 호출하면 좀비 프로세스의 프로세스 식별자와 프로세스 표 안의 기록이 그제야 사라짐.

만약 부모가 wait() 없이 종료되어 자식 프로세스가 고아 orphan가 된다면? 유닉스는 이때 init 프로세스가 자식의 양부모가 됨. init은 정기적으로 wait()을 호출하기에 고아 프로세스의 종료 상태를 받아 제대로 해제해줄 수 있게 됨.

3.3.2.1. 안드로이드 프로세스 계층

한정된 메모리 등과 같은 제약 때문에 모바일 운영체제는 존재하는 프로세스를 종료해 한정된 시스템 자원을 반환 받아야 함. 안드로이드는 프로세스의 중요도 계층 importance hierarchy라는 것을 둠. 중요도순으로 보면:

  • 전면 프로세스 foreground process - 현재 화면에 보이는 프로세스
  • 가시적 프로세스 visible process - 전면에 직접적으로 보이지는 않지만 전면 프로세스가 참조하는 활동을 수행하는 프로세스 (전면 프로세스에 출력되는 상태에 대한 작업을 수행하는 프로세스)
  • 서비스 프로세스 service process - 배경 프로세스랑 비슷한데, 사용자에게 보이는 활동을 수행 (음악 스트리밍 등)
  • 배경 프로세스 background process - 활동을 수행하는데 사용자에게는 보이지 않음
  • 빈 프로세스 empty process - 어떠한 어플케이션의 활성 성분을 포함하지 않는 프로세스

안드로이드는 최대한 높은 수준의 중요도 부여. 만약 프로세스가 서비스를 제공하면서도 가시적이면 둘 중 더 중요도가 높은 가시적 프로세스로 간주.

게다가 안드로이드 개발에서 실무적인 관점으로 보면 라이프 사이클에 대한 가이드라인을 따라야 함.

다중 프로세스 구조 - 크롬 브라우저

많은 웹사이트는 JavaScript, Flash, HTML5 등의 활성 콘텐츠를 포함. 얘네 버그 좀 있고 반응 속도도 느려서 웹 브라우저 크래시 될 수도 있음. 근데 요즘 브라우저는 탭 브라우징을 제공함.

탭이 하나라도 크래시나면 전체 프로세스가 크래시남.

구글의 크롬 웹 브라우저는 이걸 다중 프로세스 구조로 해결함. 세 가지 프로세스가 있음:

  • 브라우저 browser 프로세스는 사용자 인터페이스와 디스크, 네트워크 입출력 관리. 크롬 시작되면 오로지 하나의 브라우저 프로세스 생성함.
  • 렌더러 renderer 프로세스가 웹 페이지 렌더링함. 일반적으로 새 탭에서 웹 사이트 열릴 때마다 새 렌더러 프로세스가 생김.
  • 플러그인 plug-in 프로세스.

다중 프로세스법의 장점은 웹사이트가 서로 독립적으로 실행된다는 것. 게다가 렌더러 프로세스는 샌드박스 sandbox에서 실행되어 디스크와 네트워크 입출력에 대한 접근이 제한되어 보안 문제 해결.

3.4. 프로세스 간 통신

프로세스가 시스템의 다른 프로세스와 자료를 공유하지 않으면 독립적 independent임. 시스템의 다른 프로세스에 의해 영향을 받거나 줄 수 있으면 협력적 cooperating임.

협력이 필요한 이유:

  • 정보 공유 information sharing. 여러 어플리케이션이 같은 정보를 사용해야할 수도. 이 정보를 동시에 접근할 수 있는 방안 제공해야.
  • 연산 속도 향상 computation speedup. 특정 작업을 빠르게 처리하려면 여러 부분으로 나눠서 병렬적으로 처리하면 됨. 컴퓨터가 다중 프로세스 코어가 있어야만 가능.
  • 모듈성 modularity. 시스템 함수들을 분리된 프로세스나 스레드로 나눠야 함.

협력 프로세스는 프로세스 간 통신 interprocess communication (IPC) 메커니즘이 있어야 자료 교환 가능. 두 가지 핵심 모델: 공유 메모리 shared memory메시지 전달 message passing.

보통 대부분 운영체제에서 둘 다 구현되어있음. 메시지 전달은 적은 규모의 자료 주고 받을 때 유용. 피할 충돌이 없으니까. 분산 체계의 경우 공유 메모리보다 더 구현하기 쉬움. 공유 메모리는 메모리 전달보다는 빠름. 메시지 전달 시스템이 보통 시스템 호출을 사용해서 구현되어있어서 커널을 간섭하는 작업을 해야해서 좀 시간을 먹음. 공유 메모리에선 공유 메모리 영역 만드는 것 빼면 시스템 호출할 일이 없음.

3.5. 공유 메모리 체제에서 IPC

보통 공유 메모리 영역은 공유 메모리 영역을 생성하는 프로세스의 주소 공간에 위치하게 됨. 이 공유 메모리 영역을 통해 통신하려면 자기 주소 공간에 이걸 붙여줘야 함. 원래 운영체제는 한 프로세스가 다른 프로세스의 메모리 접근하는 거 막는다고 했는데, 이 경우 둘이 접근 가능하다고 동의해야 함. 공유할 자료의 서식, 위치는 운영체제가 아니라 프로세스가 정함. 같은 위치에 동시에 쓰고 있지 않도록 프로세스가 보장해야 함.

협력 프로세스에서 흔히 발생하는 생산자-소비자 문제. 생산자 producer소비자 consumer가 소비할 정보를 생산함. 이와 비슷한 구조가 클라이언트-서버 패러다임. 보통 서버가 생산자고 클라이언트가 소비자.

이 문제를 해결하는 한 방법이 공유 메모리. 생산자가 채우고 소비자가 비우는 버퍼를 만들면 됨. 소비자가 한 아이템을 소비하는 동안 생산자도 한 아이템 생산 가능. 아직 생산 안 된 아이템을 소비하지 않도록 서로 동기화되어야 함.

버퍼 두 가지: 무한 버퍼 unbounded buffer에는 버퍼 크기의 한계가 없음. 생산자는 무한정 생산 가능. 유한 버퍼 bounded buffer는 고정 크기 버퍼 사용. 소비자는 버퍼가 비어 있을 때, 생산자는 버퍼가 가득 찼을 때 대기해야 함.

생산자와 소비자 프로세스 간 공유하는 메모리 영역에 다음 변수가 있다고 가정:

#define BUFFER_SIZE 10

typedef struct {
    ...
} item;

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

공유 버퍼는 두 논리 포인터 inout을 통해 원형 배열로 구현. in이 버퍼의 다음으로 빈 위치. out은 버퍼의 첫번째로 차있는 위치. in == out일 때 버퍼는 빈 상태고, ((in + 1) % BUFFER_SIZE) == out일 때 버퍼 가득 차있음.

다음은 생산자의 코드:

item next_produced;

while (true) {
    /* next_produced에 아이템 생산 */
    
    while ((in + 1) % BUFFER_SIZE) == out) {
    	; /* 아무 것도 안 함 */
    }
    
    buffer[in] = next_produced;
    in = (in + 1) % BUFFER_SIZE;
}

다음은 소비자의 코드:

item next_consumed;

while (true) {
    while (in == out) {
    	; /* 아무 것도 안 함 */
    }
    
    next_consumed = buffer[out];
    out = (out + 1) % BUFFER_SIZE;
    
    /* next_consumed에 있는 아이템을 소비 */
}

버퍼에 최대 BUFFER_SIZE - 1 개의 아이템 저장 가능.

위의 코드에서는 동시에 소비 / 생산하려 할 때의 문제를 처리하진 않음.

3.6. 메시지 전달 체제에서의 IPC

메시지 전달 시설을 통해 협력 프로세스 간 통신.

같은 주소 공간 공유하지 않고도 통신과 행동 동기화 가능. 특히 분산 환경에 적합.

메시지 전달 시설은 다음 두 연산 제공:

  • send(message)
  • receive(message)

메시지 크기는 고정 시킬 수도 있고 가변적일 수도. 고정 시키면 시스템 수준에서 구현이 상당히 간단해짐. 대신 프로그래밍은 어려워짐. 반대로 가변 크기 메시지는 시스템 수준에서 구현 복잡해지는데 프로그래밍은 쉬워짐.

프로세스 P와 Q가 서로 통신하려면 서로에게 반드시 메시지를 받아야 함. 즉, 통신 연결 communication link이 존재해야 함. 실제 물리적인 연결법은 모르겠고 논리적인 연결법만 알아보도록 함. 구현하는 법:

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

3.6.1. 명명

통신할 프로세스끼리 서로 어떻게 부를지 알아야 함.

직접 통신 direct communication에서는 수신자나 송신자의 이름을 명시적으로 불러주어 통신함.

  • send(P, message) - 프로세스 P에 message 전달
  • receive(Q, message) - 프로세스 Q로부터 message 전달 받기

이 경우 다음 특징을 가짐:

  • 통신할 프로세스 짝마다 자동으로 연결됨
  • 오로지 두 프로세스끼리만 연결 가능
  • 각 짝마다 오로지 하나의 연결만 존재

즉, 대칭성 symmetry를 가짐. 송신자 프로세스랑 수신자 프로세스 서로 이름을 불러줘야 통신 가능. 이거 변형한게 비대칭성 asymmetry를 갖는 경우. 송신자만 이름을 부르고, 수신자는 굳이 송신자를 안 불러도 됨:

  • send(P, message) - 프로세스 P에 message 전달
  • receive(id, message) - message 받음. id 변수가 통신이 발생한 프로세스의 이름이 됨

단점은 둘 다 모듈성에 한계가 있음. 한 프로세스의 식별자가 바뀌면 나머지에서도 처리해줘야 함. 이런 무지성 코딩 hard-coding법을 사용할 경우 식별자를 명시해야하므로 간접법을 더 선호하게 됨.

간접 통신 indirect communication에서는 메시지가 우편함 mailbox이나 포트 port를 중간자로 놓음. 각 우편함에는 정수 값이 있어 식별 가능.

  • send(A, message) - 우편함 A에 message 전달
  • receive(A, message) - 우편함 A로부터 message 전달 받기

여기서 통신 연결의 특징:

  • 두 프로세스 간 공유하는 우편함이 있을 때 둘 간의 연결이 생김
  • 두 개 이상 프로세스 간 연결 가능
  • 통신 프로세스 짝마다 여러 연결, 즉 여러 우편함 가능.

프로세스 P1, P2, P3 셋이 우편함 A를 전부 공유할 때, 얘네 전부 A에 메시지 보내고 A로부터 receive() 호출. 그럼 P1가 보낸 메시지를 누가 얻느냐를 결정하는 규칙 정해야:

  • 최대 두 개 프로세스 간 연결 가능하도록
  • 최대 한 프로세스가 한 순간에 receive() 연산 실행 가능하도록
  • 임의로 어떤 프로세스가 이 메시지를 받을 수 있는지 선택하게 함. 어떤 프로세스가 처리할지에 대한 알고리듬(라운드 로빈 round robin 등) 써줘도 됨.

우편함은 운영체제나 프로세스나 둘 다 소유 가능. 프로세스 소유(즉 프로세스의 주소 공간에 있다면)라면 소유주(우편함에 송신)랑 사용자(우편함으로부터 수신)를 구분해야. 소유주가 고유하니 누가 메시지가 받아야할 지를 고민 안 해도 됨. 소유주 프로세스 종료하면 우편함도 사라짐. 이 우편함에 미시지 보내던 모든 프로세스는 이 우편함 더 이상 없다는 노티 받아야 함.

반대로 운영체제 우편함을 소유하는 경우 다음 기능 제공해야:

  • 우편함 생성
  • 우편함 통해 메시지 송신 및 수신
  • 우편함 삭제

새 우편함 생성하는 프로세스는 자연스럽게 해당 우편함의 소유주가 됨. 초기엔 소유주만이 이 우편함으로 메시지 수신 받을 수 있음. 소유권이나 수신권이 적합한 시스템 호출에 따라 다른 프로세스에 전달될 수도 있음. 당연히 우편함마다 여러 수신자가 있을 수도 있음.

3.6.2. 동기화

send()receive() 구현법에는 여러 설계가 있음. 메시지 전달은 블로킹 blocking / 논블로킹 nonblocking, 다시 말해 동기 synchronous비동기 asynchronous 방법이 있음.

  • 동기 송신 blocking send. 수신 프로세스 혹은 우편함에 의해 메시지가 수신될 때까지 송신 불가
  • 비동기 송신 nonblocking send. 송신 프로세스가 메시지 송신을 하면 바로 다시 재개
  • 동기 수신 blocking receive. 수신 받을 메시지가 없으면 수신 불가
  • 비동기 수신 nonblocking receive. 유효한 메시지나 널을 둘 다 받음

send()receive() 조합 가능. send()receive() 둘 다 동기라면 수신자와 송신자 간 랑데부 randezvous가 있음. 이러면 생산자-소비자 문제 해결.

message next_produced;

while (true) {
    /* next_produced에 아이템 생산 */
    
    send(next_produced);
}
message next_consumed;

while (true) {    
    send(next_consumed);
    
    /* next_consumed의 아이템 소비 */
}

3.6.3. 버퍼링

통신이 직간접이든 어쨋든 교환할 메시지는 임시 큐에 저장됨. 세 가지 방법으로 구현:

  • 무용량 zero capacity. 최대 길이가 0. 안에 메시지 보관 불가. 이 경우 송신자는 반드시 송신자에 대해 동기 송신을 해야 함.
  • 유한 용량 bounded capacity. 큐는 최대 크기 n. 최대 n 개의 메시지 저장 가능. 가득 차있으면 자리 날 때까지 송신 불가.
  • 무한 용량 unbounded capacity. 이론상 무한 크기. 송신 무조건 됨.

무용량을 버퍼링 없는 메시지 체제라고도 부름. 나머지는 자동 버퍼링 체제라 부름.

3.7. IPC 체제 예시

3.7.1. POSIX 공유 메모리

POSIX 공유 메모리는 메모리에 매핑된 파일, 즉 파일의 공유 메모리 영역을 갖는 파일로 구성됨. 프로세스는 공유 메모리 개체를 shm_open() 시스템 호출로 생성해야:

fd = shm_open(name, O_CREAT | O_RDWR, 0666);

첫번째 매개변수가 공유 메모리 개체 이름. 뒤의 매개변수는 만약 공유 메모리 개체가 존재하지 않으면 생성(O_CREAT)하고 이 개체는 읽기와 쓰기 전용으로 열 것(O_RDWR)이라는 뜻. 마지막 매개변수는 공유 메모리 개체의 파일 접근 허가를 의미. 성공적이면 공유 메모리 개체의 파일을 설명하는 정수를 반환.

개체 만들었으면 ftruncate() 함수로 개체의 크기를 바이트 단위로 설정:

ftruncate(fd, 4096);

개체 크기를 4096 바이트로.

마지막으로 mmap() 함수로 공유 메모리 개체를 포함하는 메모리에 매핑된 파일을 만듦. 해당 파일에 대한 포인터 반환해서 공유 메모리 개체 접근하는데 사용.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <sys/mman.h>

int main()
{
    /* 공유 메모리 개체의 크기 (바이트 단위) */
    const int SIZE = 4096;
    /* 공유 메모리 개체의 이름 */
    const char *name = "OS";
    /* 공유 메모리에 쓸 문자열 */
    const char* message_0 = "Hello";
    const char* message_1 = "World!";
    
    /* 공유 메모리 파일 설명자 */
    int fd;
    /* 공유 메모리 개체를 가리키는 포인터 */
    char *ptr;
    
    /* 공유 메모리 개체 생성 */
    fd = shm open(name,O CREAT | O RDWR,0666);
    
    /* 공유 메모리 개체 크기 설정 */
    ftruncate(fd, SIZE);
    
    /* 공유 메모리 개체를 메모리에 매핑 */
    ptr = (char*) mmap(0, SIZE, PROT READ | PROT WRITE, MAP SHARED, fd, 0);
    
    /* 공유 메모리 개체에 쓰기 */
    sprintf(ptr,"%s",message 0);
    ptr += strlen(message 0);
    sprintf(ptr,"%s",message 1);
    ptr += strlen(message 1);
    
    return 0;
}

MAP_SHARED 플래그를 통해 공유 메모리 개체가 수정되면 해당 개체를 공유하는 모든 프로세스에게도 해당 수정 사항이 적용된다는 뜻.

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <sys/mman.h>

int main()
{
    /* 공유 메모리 개체의 크기 (바이트 단위) */
    const int SIZE = 4096;
    /* 공유 메모리 개체의 이름 */
    const char* name = "OS";
    /* 공유 메모리 파일 설명자 */
    int fd;
    /* 공유 메모리 개체를 가리키는 포인터 */
    char* ptr;
    
    /* 공유 메모리 개체 열기 */
    fd = shm open(name, O RDONLY, 0666);
    
    /* 공유 메모리 개체 메모리에 매핑 */
    ptr = (char*) mmap(0, SIZE, PROT READ | PROT WRITE, MAP SHARED, fd, 0);
    
    /* 공유 메모리 개체로부터 읽기 */
    printf("%s",(char *)ptr);
    
    /* 공유 메모리 개체 지우기 */
    shm unlink(name);
    
    return 0;
}

소비자도 shm_unlink()로 공유 메모리 구역을 없애줘야 함.

3.7.2. 마하 메시지 전달

마하는 분산 체제용으로 설계. 이후에 데탑이랑 모바일로 확장. 나중에 macOS, iOS 운영체제에 포함.

여러 작업의 생성과 삭제 지원. 작업은 프로세스랑 유사한데 다중 스레드 제어가 있고 관련 자원이 적음. 대부분의 마하 통신은 메시지 message로 처리. 마하에서는 우편함을 포트 port라 부름. 유한 크기, 단일 방향성임. 양방향 통신하려면 메시지를 어떤 한 포트에 송신하면, 이에 대한 답장이 또다른 답장 reply 포트로 옴. 각 포트엔 여러 송신자가 있지만 오로지 한 명의 수신자가 있음. 포트로 작업, 스레드, 메모리, 프로세서 같은 자원 표현. 메시지 전달은 개체 지향적으로 자원 접근. 같은 호스트 혹은 분산 체제에서의 서로 다른 호스트에서 메시지 전달이 임의의 두 포트 간 발생 가능.

각 포트엔 포트 권한 port right의 잡힙이 있음. 작업이 포트랑 상호작용할 때 필요한 기능들을 의미. 포트를 생성한 작업이 해당 포트의 소유주이므로 해당 작업만 그 포트로부터 수신 가능. 소유주는 포트의 기능도 조작 가능. 답장 포트 만들 때 조작. 포트 권한은 작업 수준을 갖기에 같은 작업에 존재하는 모든 스레드는 동일한 포트 권한 가짐.

작업 생성되면 두 개의 특수 포트 작업 셀프 task self 포트와 노티 notify 포트도 생성. 커널이 작업 셀프 포트에 대한 수신 권한이 있어서 작업이 커널에 메시지 보낼 수 있음. 커널은 노티 포트에 이벤트 발생 노티 송신 가능(작업이 수신 권한이 있다는 뜻).

mach_port_allocate() 함수로 새 포트 생성하고 메시지 큐를 위한 공간 할당. 포트의 권한 설정. 각 포트 권한이 곧 해당 포트의 이름이고, 포트는 오로지 권한으로만 접근 가능. 단순 정수값이고 유닉스의 파일 설명자랑 비슷함:

mach_port_t port;	// 포트 권한의 이름

mach_port_allocate(
    mash_task_self(),	// 자기 자신을 가리키는 작업
    MACH_PORT_RIGHT_RECEIVE,	// 이 포트의 권한
    &port);	// 포트 권한의 이름

각 작업은 부트스트랩 포트 bootstrap port에 대한 접근이 있어 작업이 생성한 포트를 범시스템 부트스트랩 서버 bootstrap server에 등록할 수 있음. 등록되면 다른 작업이 이 포트를 등록된 목록에서 찾아서 해당 포트에 송신할 권한을 얻을 수도.

각 포트에 대한 큐는 무한 크기. 초기엔 빈 상태. 포트에 메시지 오면 큐에 메시지 복사. 모든 메시지는 같은 우선순위 가지며 신뢰성 보장하며 전달. 같은 송신자의 여러 메시지가 선입선출 순으로 큐가 되는 걸 보장하긴 하는데 절대적 순서를 보장하지는 않음. 즉 서로 다른 송신자 둘이면 순서를 알 수 없음.

마하 메시지의 내용:

  • 고정 크기 메시지 헤더. 메시지에 대한 메타데이터, 즉 메시지 크기, 송신 포트, 수신 포트 등 포함. 보통 송신 스레드는 답장을 기대하므로 송신 포트의 이름을 수신 작업에 전달해줘서 답장 보낼 때 "반환 주소"로 사용
  • 가변 크기 본문. 자료를 갖고 있음.

메시지는 간단할 수도, 복잡할 수도. 간단한 메시지는 일반적인, 구조화되지 않은 사용자 자료. 커널이 해석 X. 복잡한 메시지는 자료를 갖는 메모리를 가리키는 포인터나 다른 작업에 포트 권한 넘기기 등. 간단한 메시지는 메시지의 자료 복사 및 포장만 하면 끝인데 복잡한 메시지는 자료가 저장된 메모리 위치, 즉 포인터만 처리하면 됨.

mach_msg()가 메시지 송수신 둘 다 처리. 함수의 매개변수가 MACH_SEND_MSGMACH_RCV_MSG에 따라 송수신처리.

사용자 프로그램에서 mach_msg() 호출해서 메시지 전달. 이 함수는 mach_msg_trap() 함수, 즉 마하 커널에 대한 시스템 호출 실행. 커널 내부에서 mach_msg_trap()mach_overwrite_trap() 호출하여 실제 메시지 전달 처리.

#include<mach/mach.h>

struct message {
    mach_msg_header_t header;
    int data;
};

mach_port_t client;
mach_port_t server;

    /* 클라이언트 코드 */

struct message message;

// 헤더 설정
message.header.msgh_size = sizeof(message);
message.header.msgh_remote_port = server;
message.header.msgh_local_port = client;

// 메시지 송신
mach_msg(&message.header, // 메시지 헤더
    MACH_SEND_MSG, // 메시지 송신
    sizeof(message), // 송신한 메시지 크기
    0, // 수신한 메시지의 최대 크기 (선택 사항)
    MACH_PORT_NULL, // 수신 포트의 이름 (선택 사항)
    MACH_MSG_TIMEOUT_NONE, // 타임 아웃 없음
    MACH_PORT_NULL // 노티 포트 없음
);

    /* 서버 코드 */

struct message message;

// 메시지 수신
mach_msg(&message.header, // 메시지 헤더
    MACH_RCV_MSG, // 메시지 송신
    0, // 송신한 메시지 크기
    sizeof(message), // 수신 받은 메시지 크기
    server, // 수신 포트 이름
    MACH_MSG_TIMEOUT_NONE, // 타임 아웃 없음
    MACH_PORT_NULL // 노티 포트 없음
);

송수신 연산 자체는 유연함. 만약 송신했을 때 포트의 큐가 가득 차있을 때 어떻게 할지에 대한 선택을 제공 가능(mach_msg()의 매개변수 형태로):

  1. 공간 날 때까지 무한정 대기
  2. 최대 n 밀리초 대기
  3. 대기하지 말고 즉시 반환
  4. 임시로 메시지 캐시하여 운영체제에 맡겨둠. 큐에 공간 나면 송신자에게 노티 메시지가 감. 송신 스레드마다 오로지 한 메시지만 맡길 수 있음.

4번 선택지는 서버 작업용임. 요청 끝나면 서버가 서비스를 요청한 작업에게 일회용 답변을 보내곤 함. 근데 답장 큐가 가득찼다고 다른 서비스 요청을 못 해주면 안되잖아.

메시지 복사 때문에 성능 문제. 가상 메모리 관리 기법으로 복사 연산 피하려고는 함. 근본적으로 마하는 송신자의 메시지를 갖는 주소 공간을 수신자의 주소 공간에 매핑함. 그래서 결국 둘 다 같은 메모리 접근 가능해 메시지 자체는 실제 복사되지 않음. 이 메시지 관리 기법이 성능 개선해주긴 했는데 내부체제 메시지 간에만 가능.

3.7.3. 윈도우즈

모듈성을 통해 기능성을 높이고 새 기능 구현 시간을 단축시킨 현대적인 설계. 다중 운영체제, 혹은 하위 시스템 subsystem을 제공. 어플리케이션 프로그램은 이 하위 시스템과 메시지 전달 메커니즘으로 통신. 어플리케이션 프로그램은 하위 시스템 서버의 클라이언트인 것.

윈도우즈의 메시지 전달 시설은 고급 로컬 프로시저 호출 advanced local procedure call (ALPC). 표준 원격 프로시저 호출(RPC)과 유사하지만, 윈도우즈에 좀 더 최적화됨. 마하처럼 포트 개체로 프로세스 간 통신 유지. 연결 포트 connection port통신 포트 communication port 두 가지.

서버 프로세스가 모든 프로세스가 볼 수 있는 연결 포트 개체를 제공함. 클라이언트가 하위 시스템에서 서비스를 요구할 경우 서버의 통신 포트 개체에 핸들을 열어 통신 요청을 해당 포트에 보냄. 서버가 그럼 채널을 만들어 클라이언트로의 핸들을 반환. 채널에는 한 쌍의 개인 통신 포트 생성: 하나는 클라이언트-서버 메시지, 나머지 하나는 서버-클라이언트 메시지. 추가적으로 통신 채널이 콜백 메커니즘을 제공해 클라이언트와 서버가 답장을 기대하는 요청을 받아 들임.

ALPC 채널 생성 시 다음 중 하나의 메시지 전달 기법 선택:

  1. 크기가 작은 메시지(256 바이트)의 경우 포트의 메시지 큐를 중간 저장소로 사용해 메시지가 한 프로세스에서 다른 프로세스로 복사됨
  2. 크기가 큰 메시지는 구역 개체 section object을 통해 전달. 채널에 대한 공유 메모리 영역임.
  3. 자료량이 구역 개체에 넣기에 너무 클 경우 서버 프로세스가 클라이언트의 주소 공간에 직접 읽고 쓸 수 있는 API 제공.

ALPC 시설은 윈도우즈 API의 일부분이 아니기 때문에 어플리케이션 프로그래머들에겐 안 보임. 윈도우즈 API 사용할 경우 표준 원격 프로시저 호출을 사용. RPC은 간접적으로 ALPC 프로시저 호출로 처리됨. 많은 커널 서비스가 ALPC로 클라이언트 프로세스와 통신.

3.7.4. 파이프

파이프 pipe란 두 프로세스 간 통신을 위한 수도관. 초기 유닉스 체제의 초기 IPC 메커니즘 중 하나. 한계점도 있지만 매우 간단한 방법의 프로세스 간 통신. 네 가지 문제를 고려하여 구현:

  1. 파이프가 양방향성 통신 허용? 아니면 일방향적?
  2. 양방향 통신 가능하면 반이중 통신방식(데이터가 한 순간에는 한 쪽으로 밖에 진행 못함)인지, 전이중 통신방식(데이터가 동시에 양방향으로 진행 가능)인지?
  3. 통신할 프로세스 간 관계(부모-자식처럼)가 반드시 존재?
  4. 파이프가 네트워크에서 통신 가능? 아니면 같은 기계에서만 가능?
3.7.4.1. 일반 파이프

일반 파이프는 기본적인 생산자-소비자 방식의 프로세스 지원. 생산자가 파이프의 한 끝단에서 쓰기를 함(끝에 쓰기 write end). 소비자는 반대 끝단에서 읽음(끝에 읽기read end). 결과적으로 일반 파이프는 일방향적.

유닉스에서 일반 파이프는 다음 함수로 생성:

pipe(int fd[])

int fd[]로 접근할 수 있는 파이프 생성해줌. fd[0]가 읽는 쪽 끝부분, fd[1]이 쓸 끝 부분. 유닉스는 파이프를 특수한 종류의 파일로 간주. 그래서 일반적인 read()write() 시스템 호출로 접근 가능.

일반 파이프는 생성한 프로세스 외에는 접근 불가. 보통 부모 프로세스가 파이프 만들어서 자식 프로세스와 통신. 파이프는 특수한 종류의 파일이니 자식이 파이프를 부모 프로세스로부터 상속 받음.

#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#define BUFFER_SIZE 25
#define READ_END 0
#define WRITE_END 1

int main(void)
{
    char write_msg[BUFFER_SIZE] = "Greetings";
    char read_msg[BUFFER_SIZE];
    int fd[2];
    pid_t pid;
    
    /* 파이프 생성 */
    if (pipe(fd) == -1) {
    	fprintf(stderr,"Pipe failed");
        return 1;
    }
    
    /* 자식 프로세스 fork */
    pid = fork();
    if (pid <0) {/* 오류 발생 */
    	fprintf(stderr, "Fork Failed");
    	return 1;
    }
    
    if (pid >0) {/* 부모 프로세스 */
    	/* 사용하지 않는 파이프의 끝 부분 닫기 */
    	close(fd[READ END]);
    	/* 파이프에 쓰기 */
    	write(fd[WRITE_END], write_msg, strlen(write_msg)+1);
    	/* 파이프 쓰기 쪽 끝부분 닫기 */
    	close(fd[WRITE_END]);
    } else {/* 자식 프로세스 */
    	/* 사용하지 않는 파이프의 끝 부분 닫기 */
    	close(fd[WRITE_END]);
    	/* 파이프 읽기 */
    	read(fd[READ_END], read_msg, BUFFER_SIZE);
    	printf("read %s",read_msg);
    	/* 파이프 읽는 쪽 끝부분 닫기 */
    	close(fd[READ_END]);
    }
    
    return 0;
}

부모가 파이프 생성한 다음 fork()로 자식 프로세스 생성. 그 이후는 파이프를 통해 자료가 어떻게 흐르느냐에 따라 달라짐. 부모랑 자식 둘 다 초기에 파이프에서 안 쓰는 끝을 닫아줌. 파이프를 읽을 프로세스가 쓰는 쪽이 파이프의 한 쪽을 닫을 때 파일 끝(read()가 0을 반환)을 인식했음을 확인해야 함.

윈도우즈 체제에서는 일반 파이프를 익명 파이프 anonymous pipe라 부름. 유닉스랑 유사하게 작동. 여기에 추가적으로 파이프에 읽고 쓰는 걸 일반적인 ReadFile(), WriteFile() 함수로 처리. 윈도우즈 API의 파이프 생성은 CreatePipe(). 매개변수 네 개인데, 각각 핸들 전달. (1) 읽기용 (2) 쓰기용 (3) 자식 프로세스가 파이프의 핸들을 상속 받을 것을 의미하는 STARTUPINFO 구조체 (4) 파이프의 크기 (바이트 단위).

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

#define BUFFER_SIZE 25

int main(VOID)
{
    HANDLE ReadHandle;
    HANDLEWriteHandle;
    STARTUPINFO si;
    PROCESS_INFORMATION pi;
    char message[BUFFER_SIZE] = "Greetings";
    DWORD written;
    
    /* 파이프 상속 권한을 위해 보안 속성 설정 */
    SECURITY_ATTRIBUTES sa = {sizeof(SECURITY_ATTRIBUTES),NULL,TRUE};
    /* 메모리 할당 */
    ZeroMemory(&pi, sizeof(pi));
    
    /* 파이프 생성 */
    if (!CreatePipe(&ReadHandle, &WriteHandle, &sa, 0)) {
    	fprintf(stderr, "Create Pipe Failed");
        return 1;
    }
    
    /* 자식 프로세스를 위한 `START_INFO` 구조체 생성 */
    GetStartupInfo(&si);
    si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
    
    /* 표준 입력을 파이프의 읽기 끝부분으로 리다이렉트 */
    si.hStdInput = ReadHandle;
    si.dwFlags = STARTF_USESTDHANDLES;
    
    /* 자식이 파이프의 쓰기 끝 상속 못하도록 */
    SetHandleInformation(WriteHandle, HANDLE_FLAG_INHERIT, 0);
    
    /* 자식 프로세스 생성 */
    CreateProcess(NULL, "child.exe", NULL, NULL, 
    		TRUE, /* 핸들 상속 */
            	0, NULL, NULL, &si, &pi);
    
    /* 안 쓰는 끝부분 닫기 */
    CloseHandle(ReadHandle);
    
    /* 부모가 파이프에 쓰기 */
    if (!WriteFile(WriteHandle, message,BUFFER_SIZE, &written, NULL)) {
    	fprintf(stderr, "Error writing to pipe.");
    }
    
    /* 쓰기 끝 부분 닫기 */
    CloseHandle(WriteHandle);
    
    /* 자식이 종료할 때까지 대기 */
    WaitForSingleObject(pi.hProcess, INFINITE);
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
    return 0;
}
#include <stdio.h>
#include <windows.h>

#define BUFFER_SIZE 25

int main(VOID)
{
    HANDLE Readhandle;
    CHAR buffer[BUFFER_SIZE];
    DWORD read;
    
    /* 파이프의 읽기 핸들 겟 */
    ReadHandle = GetStdHandle(STD_INPUT_HANDLE);
    
    /* 자식이 파이프에서 읽기 */
    if (ReadFile(ReadHandle, buffer, BUFFER_SIZE, &read, NULL)) {
    	printf("child read %s",buffer);
    } else {
    	fprintf(stderr, "Error reading from pipe");
    }
    
    return 0;
}

자식 프로세스가 자동으로 부모가 생성한 파이프를 상속 받는 유닉스와는 달리 윈도우즈는 상속 받을 속성을 명시해줘야 함. 이거 하려고 SECURITY_ATTRIBUTE을 핸들 상속 받을 수 있게 초기화해주어 프로세스의 표준 입출력용 핸들을 파이프의 읽기 쓰기 핸들에 리다이렉트해줌. 자식은 읽을테니 부모가 자식의 표준 입력이 파이프 핸들을 읽도록 함. 파이프는 반이중 통신 방식이라 자식이 쓰는 끝부분을 상속하지 않도록 주의. 프로세스 생성했던 예제 코드와 비교하면 프로세스 생성 때 다섯번째 매개변수가 상속을 위한 TRUE라는 점에서 다름. 파이프에서 읽기 전에 파이프 읽기 핸들을 GetStdHandle()을 통해 얻음.

일반 파이프는 유닉스랑 윈도우스 둘 다 부모-자식 관계. 즉, 오로지 같은 기계 내에서의 통신만 가능.

3.7.4.2. 명명된 파이프

일반 파이프는 프로세스가 통신할 때만 존재.

명명된 파이프는 좀 더 강력한 통신 도구. 양방향 통신 가능, 부모-자식 관계 필수 아님. 여러 프로세스가 사용 가능. 일반적으로 명명된 파이프엔 여럿이 쓰기를 함. 통신 프로세스가 끝나도 파이프는 안 없어짐. 구현 방법은 달라도 유닉스랑 윈도우즈 둘 다 갖고 있음.

유닉스 체제에서는 선입선출. 생성되면 파일 시스템의 일반적인 파일처럼 작동. mkfifo() 시스템 호출로 선입선출 만들고 일반적인 open(), read(), write(), close() 시스템 호출로 조작. 명시적으로 지워주기 전까지 존재. 선입선출이 양방향 통신은 되는데 반이중 통신 방식만 허락됨. 양방향으로 자료 왔다 갔다하게 하려면 일반적으로 선입선출 두 개 사용. 그리고 둘 다 같은 기계에 있어야 함. 기계 간 통신하려면 소켓 사용해야.

윈도우즈에서는 유닉스보다 더 풍부한 통신 메커니즘 제공. 전이중 통신 방식 가능, 통신 프로세스는 같은 기계 내에서도 되고 달라도 됨. 유닉스는 바이트 기준의 자료만 전송 가능한데 윈도우즈는 바이트 기준이든 메시지 기준이든 가능. CreateNamedPipe() 함수로 명명된 파이프 생성. ConnectNamedPipe() 함수로 클라이언트가 연결. ReadFile()WriteFile() 함수로 통신.

실무에서 파이프

유닉스의 명령 줄 환경에서 한 명령어의 출력이 다른 명령어의 입력으로 사용될 때 자주 사용함. ls 명령어는 경로 목록 보여줌. 엄청 긴 경로는 보기 불편해서 less 명령어로 한 화면에서만 보여주고, 특정 키로 위 아래로 움직일 수 있게 해줌. lsless 간 파이프 설정해주면 ls의 출력을 less의 입력으로 받을 수 있게 됨. 여기서 파이프 만들어주는 게 | 문자.

ls | less

ls가 생산자, less가 소비자. 윈도우즈 체제는 DOS 쉘에서 more 명령어가 less의 역할. 여기서도 | 문자 사용. ls 대신 dir 명령어 사용.

dir | more

3.8. 클라이언트-서버 체제에서의 통신

두 가지 방법: 소켓과 원격 프로시저 호출(RPC). 안드로이드는 원격 프로시저로 같은 체제 내의 프로세스 간 통신으로 활용하기도.

3.8.1. 소켓

소켓 socket은 통신의 끝단을 의미. 네트워크 상에서 통신하는 두 프로세스는 각각 소켓 가짐. 소켓은 고유한 IP 주소에 포트 번호 가짐. 일반적으로 소켓은 클라이언트-서버 구조. 서버는 특정 포트에 귀를 기울여 클라이언트 요청 올 때까지 대기. 특정 서비스 구현하는 서버(SSH, FTP, HTTP 등)는 잘 알려진 well-known 포트에 귀 기울임. 1024번 이하의 포트를 잘 알려진 포트라 함.

클라이언트가 통신 요청하면 호스트 컴퓨터가 포트 할당함. 1024보다 큰 값 가짐.

모든 통신은 고유함. 호스트 X의 다른 프로세스가 동일 웹 서버랑 통신하려면 또 다른 포트 번호 할당해야 함.

자바는 세 가지 소켓 제공. 연결형 connection-oriented(TCP)은 Socket 클래스로, 비연결형 connection-less(UDP)은 DatagramSocket 클래스로. MulticastSocket 클래스는 DatagramSocket의 하위 클래스. 다중 수신자에게 자료 송신 가능.

연결형 TCP 소켓 예시. 클라이언트가 서버에 현재 날짜와 시간 요청. 포트 6013를 듣는다고 가정. 연결 수신받으면 서버가 클라이언트에게 날짜와 시간 반환.

import java.net.*;
import java.io.*;

public class DateServer {
    public static void main(String[] args) {
    	try {
            ServerSocket sock = new ServerSocket(6013);
            
            /* 연결에 귀 기울이기 */
            while (true) {
            	Socket client = sock.accept();
            	PrintWriter pout = new PrintWriter(client.getOutputStream(), true);
            
            	/* 소켓에 날짜 쓰기 */
            	pout.println(new java.util.Date().toString());
            
            	/* 소켓 닫고 연결 듣기 재개 */
            	client.close();
            }
    	} catch (IOException ioe) {
            System.err.println(ioe);
        }
    }
}

클라이언트와 통신에 사용할 PrintWriter 개체 생성. PrintWriter 개체로 서버가 출력용 print(), println() 함수로 소켓에 쓰기 가능하게 해줌. println()으로 클라이언트에게 날짜 송신. 소켓에 날짜 썻으면 클라이언트로의 소켓 닫고 다른 요청 대기.

클라이언트는 소켓 생성하고 서버가 들을 포트에 연결하는 식으로 서버와 통신. Socket 생성하여 서버의 IP 주소와 포트에 따라 연결 요청. 연결되면 일반적인 스트림 입출력문으로 소켓으로부터 읽기 가능. 날짜 다 읽으면 소켓 닫고 종료. 이때 IP 주소 127.0.0.1은 자기 자신을 의미하는 루프백 loopback이라 부름. 즉, 같은 기계에서도 TCP/IP 프로토콜로 통신 가능. 다른 IP 주소나 실제 호스트 이름, www.westminstercollege.edu 등 사용 가능.

import java.net.*;
import java.io.*;

public class DateClient {
    public static void main(String[] args) {
        try {
          /* 서버 소켓과 연결 생성 */
          Socket sock = new Socket("127.0.0.1",6013);
          
          InputStream in = sock.getInputStream();
          BufferedReader bin = new
          BufferedReader(new InputStreamReader(in));
          
          /* 소켓으로부터 날짜 읽기 */
          String line;
          while ( (line = bin.readLine()) != null) {
              System.out.println(line);
          }
          
          /* 소켓 연결 닫기 */
          sock.close();
      } catch (IOException ioe) {
      	  System.err.println(ioe);
      }
    }
}

소켓 간 통신이 흔하고 효율적이지만 분산 프로세스 간 통신의 저수준 형태로 간주. 소켓이 오로지 구조화되지 않은 바이트의 스트림이 통신 스레드 간 교환되기 때문. 클라이언트나 서버가 그 자료를 구조화해야 함. 좀 더 고수준 통신법이 원격 프로시저 호출(RPC).

3.8.2. 원격 프로시저 호출

매우 흔한 원격 서비스가 RPC 패러다임. 네트워크 연결된 체제 간 사용할 프로시저 호출 메커니즘을 추상화한 방안으로 설계. 프로세스 간 통신과 유사. 서로 다른 기계이므로 메시지 기반 통신 사용.

RPC 통신에서 교환하는 메시지는 구조화 되어있음. 각 메시지는 원격 체제의 포트를 듣는 RPC 다이몬에 전송되며 각각 실행할 함수와 전달할 매개변수를 명시해줌. 요청한대로 함수 실행한 결과를 요청한 사람에게 별개의 메시지로 전달.

여기서 포트 port란 단순히 메시지 패킷 맨 앞의 숫자. 보통 체제 자체는 네트워크 주소가 하나지만 포트는 여러 개일 수 있어 지원하는 여러 네트워크 서비스를 구분. 원격 프로세스가 서비스가 필요하면 적절한 포트에게 메시지 전달.

RPC의 구조에 의해 클라이언트가 원격 호스트의 프로시저를 마치 로컬에서 호출하듯 하게 해줌. RPC 체제는 클라이언트 쪽에서 스텁 stub을 제공해 통신할 수 있게 해주는 자세한 내용을 숨겨줌. 보통 원격 프로시저마다 서로 다른 스텁 가짐. 클라이언트가 원격 프로시저 호출하면 RPC 체제가 적합한 스텁 호출하여 원격 프로시저에 매개변수 전달. 이 스텁은 서버의 포트 위치 파악하여 매개변수를 결집 marshal함. 이후 스텁이 메시지 전달로 서버에 메시지 전달. 서버 쪽의 스텁도 마찬가지로 메시지 받아 서버에서 프로시저 실행. 필요하다면 같은 방법으로 클라이언트에 반환 값 전송. 윈도우즈에서 스텁 코드는 마이크로소프트 인터페이스 정의 언어 Microsoft Interface Definition Language (MIDL)로 작성되어 컴파일 됨. 클라이언트-서버 프로그램 간의 인터페이스 정의해줄 때 사용하는 언어.

매개변수 결집의 문제는 클라이언트와 서버 기계 간 자료 표현이 다를 수도 있다는 것. 32비트 정수 표현의 경우 몇몇 체제(빅 엔디언 big-endian)에서는 최상위 바이트를 먼저 저장하는 반면 나머지(리틀 엔디언 little-endian)에서는 최하위 바이트를 먼저 저장. 둘 중 누가 더 "나은 건" 없고, 컴퓨터 구조마다 그냥 다른 것. 이거 해결하려고 대부분의 RPC 체제는 기계 독립적인 자료 표현을 정의. 대표적인게 외부 자료 표현 external data representation(XDR). 클라이언트 단에서 매개변수 결집할 때 기계 종속적인 자료를 XDR로 바꾸어 서버에 전송. 서버에서는 XDR 자료의 결집을 풀어 서버의 기계 종속적인 표현으로 변환.

또다른 문제는 호출의 방식. 로컬 프로시저 호출은 오로지 극한의 상황에서만 실패하지만, RPC는 흔히 발생하는 네트워크 오류 때문에 실패할 수도, 복제될 수도, 한 번 이상 실행될 수도 있음. 이 문제를 처리하려면 운영체제가 메시지가 최대 한 번이 아니라 딱 한 번만 처리되도록 해야 함. 대부분 로컬 프로시저 호출이 "딱 한 번"만 처리되는 기능을 갖지만 구현하기 더 어려움.

"최대 한 번"의 경우 각 메시지에 타임스탬프 주어서 구현. 서버는 이미 처리한 메시지들의 타임스탬프 기록해 반복된 메시지를 삭제. 이미 기록에 있는 타임스탬프를 갖는 메시지 무시. 클라이언트는 한 번 이상 더 전송하여 딱 한 번 실행되었음을 보장 받을 수도.

"딱 한 번"의 경우 서버가 요청을 절대 못 받을 경우를 없애야. 서버는 반드시 위의 "최대 한 번" 프로토콜 구현해놓고, 클라이언트에게 RPC 호출을 받아 실행했다는 확인을 전송해야. 이런 ACK 메시지는 네트워크에 매우 흔함. 클라이언트는 ACK를 받을 때까지 각 RPC 호출을 정기적으로 재전송.

또다른 문제는 서버와 클라이언트 간 통신. 표준 프로시저 호출하면 연결, 로딩, 실행 시에 일종의 바인딩이 발생해 프로시저 호출의 이름이 프로시저 호출의 메모리 주소로 교체될 수 있음. RPC에서도 클라이언트와 서버 포트 간 유사하게 바인딩을 해야 함. 근데 서버의 포트 번호를 클라이언트가 어떻게 앎?

두 가지 방법 흔히 사용. 우선 바인딩 정보를 포트 주소를 고정하는 식으로 미리 결정하는 것. 컴파일 시에 RPC 호출이 고정된 포트 번호를 가짐. 컴파일 된 순간 서버는 요청 받은 서비스의 포트 번호를 못 바꿈. 두번째는 랑데부 메커니즘으로 동적으로 바인딩하는 것. 일반적으로 운영체제는 고정된 RPC 포트에 랑데부(매치메이커 matchmaker라고도 부름) 다이몬을 제공. 클라이언트가 랑데부 다이몬에 RPC 이름 포함한 메시지 전달하여 실행할 RPC의 포트 주소 요청. 포트 번호 반환하면 프로세스가 종료할 때까지(혹은 서버가 크래시할 때까지) 해당 포트로 RPC 호출 가능. 초기 요청에 추가 오버헤드가 발생하지만 첫번째보단 유연함.

RPC는 분산 파일 체제 구현할 때 유용. RPC 다이몬과 클라이언트의 집합으로 구현. 메시지를 파일 연산을 실행할 서버의 분산 파일 체제 포트에 보냄. 메시지에는 수행할 디스크 연산을 담고 있음. 반환 메시지는 DFS 다이몬이 클라이언트 대신 해당 호출을 하여 그 결과에 해당하는 임의의 자료.

3.8.2.1. 안드로이드 RPC

RPC는 일반적으로 분산 체제의 클라이언트-서버에 대한 내용. 안드로이드에서는 프로세스 간 통신으로 쓰기도. 안드로이드 운영체제에는 바인더 binder 프레임워크에 프로세스가 다른 프로세스에 서비스 요청을 할 수 있는 RPC를 포함한 풍부한 IPC 메커니즘들이 있음.

안드로이드에서는 안드로이드 어플리케이션에 기능을 제공하는 기본 단위를 어플리케이션 성분 application component라 정의. 한 앱에는 여러 어플리케이션 성분을 가질 수도. 그런 성분 중 하나가 서비스 service. 사용자 인터페이스는 없지만 배경에서 장시간 실행할 연산을 실행하거나 원격 프로세스를 위한 작업을 실행함. 노래 듣는 거 등. 클라이언트 앱이 서비스의 bindService() 메서드를 호출하면 해당 서비스가 "바운드"가 되어 메시지 전달이는 RPC든 클라이언트-서버 간 통신이 가능해짐.

바운드 서비스는 반드시 안드로이드 클래스 Service를 상속받아 bindService()를 클라이언트가 호출할 때 호출될 onBind()를 구현해야 함. 메시지 전달의 경우 onBind() 메서드는 Messenger 서비스를 반환. 이건 일방향. 서비스가 클라이언트에 답장 보내려면 클라이언트도 Messenger 서비스 제공해야. 이게 서비스로 보낼 Message 개체의 replyTo 멤버 변수.

RPC 제공하려면 onBind() 메서드가 반드시 클라이언트가 서비스와 상호작용할 원격 개체의 메서드를 의미하는 인터페이스를 반환해야 함. 인터페이스는 일반 자바 문법으로 작성되어있으며, 안드로이드 인터페이스 정의 언어 AIDL로 스텁 파일 생성하여 원격 서비스에 대한 클라이언트 인터페이스 역할.

remoteMethod()라는 범용 원격 서비스를 AIDL과 바인더 서비스로 제공할 때 필요한 프로세스 여기서 소개. 원격 서비스를 위한 인터페이스:

/* RemoteService.aidl */
interface RemoteService {
    boolean remoteMethod(int x, double y);
}

파일명은 RemoteService.aidl. 안드로이드 개발 킷은 이걸로 .java 인터페이스와 스텁 생성. 서버는 반드시 .aidl 파일에 의해 생성된 인터페이스를 구현해야 하며, 이 인터페이스에 대한 구현이 클라이언트가 remoteMethod()를 호출할 때 호출되어야 함.

클라이언트가 bindService() 호출하면 onBind() 메서드가 서버에서 호출되어 RemoteService 개체에 대한 스텁을 클라이언트에 반환. 그럼 클라이언트는 원격 메서드를 다음과 같이 호출 가능:

RemoteService service;
...
service.remoteMethod(3, 0.14);

내부적으로 안드로이드 바인더 프레임워크가 매개변수 결집, 결집된 매개변수 전송, 서비스의 필요한 구현부 호출, 반환 값 전송 등을 처리.

3.9. 요약

  • 프로세스란 실행 중인 프로그램. 프로세스의 현재 활동 상태는 프로그램 카운터와 여러 레지스터로 표현.
  • 메모리에서의 프로세스는 네 개의 구역: (1) 텍스트, (2) 자료, (3) 힙, (4) 스택
  • 프로세스 실행될 때 상태 바뀜. 네 개의 일반적인 상태: (1) 준비, (2) 실행, (3) 대기, (4) 종료
  • 프로세스 제어 블록(PCB)은 운영체제에서의 프로세스를 표현하는 커널 자료구조.
  • 프로세스 스케줄러의 역할은 CPU에서 실행할 수 있는 프로세스 선택.
  • 운영체제는 한 프로세스에서 다른 걸로 넘어갈 때 문맥 교환.
  • fork()(유닉스)와 CreateProcess()(윈도우즈) 시스템 호출로 프로세스 생성
  • 공유 메모리로 프로세스 간 통신 시 두 개(이상)의 프로세스가 같은 메모리 영역 공유. POSIX로 공유 메모리에 대한 API 제공.
  • 메시지 전달로 두 프로세스가 메시지 교환하여 통신 가능. 마하 운영체제는 메시지 전달이 메인 프로세스 간 통신법. 윈도우즈도 일종의 메시지 전달 사용.
  • 파이프는 두 프로세스 간 통신선. 일반과 명명된 파이프 두 가지 존재. 일반 파이프는 부모-자식 관계 프로세스 간 통신을 위함. 명명된 파이프는 좀 더 일반적이고 여러 프로세스 간 통신 가능.
  • 유닉스 체제는 pipe() 시스템 호출로 일반 파이프 제공. 일반 파이프는 읽기 끝과 쓰기 끝 가짐. 부모 프로세스가 쓰기 끝으로 자료 송신, 자식 프로세스가 읽기 끝에서 자료 수신. 유닉스에서 명명된 파이프는 선입선출.
  • 윈도우즈에선 익명과 명명된 파이프 두 가지. 익명 파이프는 유닉스의 일반 파이프와 유사. 명명된 파이프는 유닉스의 선입선출보다 더 풍부한 프로세스 간 통신 제공.
  • 클라이언트-서버 간 통신은 보통 소켓과 원격 프로시저 호출 (RPC) 두 가지. 소켓은 서로 다른 기계의 두 프로세스 간 네트워크를 통한 통신. RPC 함수(프로시저) 호출 개념을 추상화해 다른 컴퓨터에 있는 프로세스에서 함수 호출 될 수 있도록 해줌.
  • 안드로이드 운영체제는 바인더 프레임워크를 통해 RPC로 일종의 프로세스 간 통신 제공.

좋은 웹페이지 즐겨찾기