Operating System Concepts, 10th Edition을 읽고 정리하기 위해 작성하는 글입니다.
POSIX 공유 메모리는 메모리 매핑 파일을 사용하여 구성되며, 공유 메모리 영역을 파일과 연결한다.
먼저 프로세스는 shm_open() 시스템 콜을 사용하여 공유 메모리 객체를 생성해야 한다. 객체가 설정되면 ftruncate() 함수를 사용하여 객체의 크기를 바이트 단위로 구성한다. 마지막으로 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()는 소비자가 액세스한 뒤 공유 메모리 세그먼트를 제거하는 함수이다.Mach 운영 체제는 분산 시스템을 위해 설계되었지만, 데스크탑과 모바일 시스템에도 적합하다.
Mach 커널은 제어 스레드가 여러 개이고 관련된 리소스가 적은 여러 작업의 생성과 제거를 지원한다. 대부분의 통신은 메시지를 통해 수행되며 메시지는 포트(port)라고 불리는 메일 박스로 전송되거나 수신된다.
양방향 통신의 경우 메시지는 포트로 전송되고 응답은 별도의 회신 포트로 전송된다. 각 포트는 발신자가 여러 개 존재할 수 있지만 수신자는 하나만 존재한다.
또, 각 포트에는 상호 작용에 필요한 기능을 실별하는 포트 권한 컬렉션이 연관된다. 포트를 생성하는 작업은 해당 포트의 소유자이며, 소유자는 유일하게 해당 포트에서 메시지를 수신할 수 있다.
MACH_PORT_RIGHT_RECEIVE 권한이 필요하다.MACH_PORT_RIGHT_SEND 권한이 필요하다.위의 예시를 보면 포트 권한 소유권이 작업 수준에 존재하며, 동일한 작업에 속한 모든 스레드는 동일한 포트 권한을 공유한다는 것을 의미한다. 따라서 동일한 작업에 속한 스레드는 각 스레드와 연관된 포트를 통해 메시지를 쉽게 교환하여 통신할 수 있다.
작업이 생성되면 두 개의 특수 포트인 작업 자체 포트(task Self port)와 알림 포트(notify port)가 생성된다. 커널은 작업 자체 포트로 부터 메시지를 받을 수 있고, 알림 포트에 이벤트 발생 알림을 보낼 수 있다.
또, 각 작업은 부트스트랩 포트(bootstrap port)에 액세스할 수 있으며, 작업은 자신이 생성한 포트를 시스템 전체 부트스트랩 서버에 등록할 수 있다. 포트가 부트스트랩 서버에 등록되면 다른 작업은 레지스트리(registry)에서 포트를 조회하여 다른 포트로 메시지를 보낼 권한을 얻을 수 있다.
예시) 포트를 생성하는 코드
mach_port_t port; // 포트 이름
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port);
mach_port_allocate() 함수는 새 포트를 생성하고 메시지 큐에 공간을 할당하며 포트에 대한 권한을 식별한다.각 포트와 연관된 큐는 크기가 유한하며 포트에 메시지가 전송되면 큐에 복사된다. Mach는 선입선출(FIFO) 순서로 큐에 들어가도록 보장한다.
메시지는 간단한 메시지에 포함되는 커널에서 해석되지 않는 사용자 데이터와 복잡한 메시지에 포함되는 데이터가 포함된 메모리 위치인 아웃 오브 라인(out-of-line) 데이터에 대한 포인터나 다른 작업에 포트 권한을 전송하는데 사용될 수 있다.
Mach 메시지는 메시지의 크기와 시작 및 도착 포트에 대한 메타데이터가 포함된 헤더와 데이터가 포함된 본문이 포함된다.
포트에 메시지가 전송될 때 큐가 가득차지 않은 경우는 메시지가 큐에 복사되지만 가득찬 경우는 무기한 대기, 최대 n밀리초 동안 대기, 즉시 반환, 메시지를 일시적으로 캐시하는 옵션이 존재한다. 캐시하는 옵션은 운영 체제에 메시지가 보관되며 큐에 공간이 생기면 다시 전송된다.
예시) 클라이언트가 서버로 메시지를 보내고 서버가 메시지를 수신하는 코드
#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()는 메시지를 보내고 받는 표준 API이다. MACH_SEND_MSG와 MACH_RCV_MSG는 전송인지 수신인지를 나타내는 매개 변수이다.mach_msg() 함수는 Mach 커널의 시스템 콜인 mach_msg_trap()을 호출한 다음 mach_overwrite_trap()을 호출하여 메시지의 실제 전달을 처리한다.메시지 시스템의 주요 문제는 발신자 포트에서 수신자 포트로 메시지를 복사하여 발생하는 성능 저하이다. Mach 메시지 시스템은 가상 메모리 관리 기술을 사용해 복사 작업을 피한다. 발신자 메시지의 주소 공간을 수신자 주소 공간에 매핑하는 방식으로 메시지 자체가 실제로 복사되지 않는다.
Windows 운영 체제는 모듈성(modularity)을 사용하여 기능을 늘리며 여러 운영 환경이나 하위 시스템(subsystem)을 지원한다. 애플리케이션 프로그램은 메시지 전달 매커니즘을 통해 이러한 하위 시스템과 통신한다.
Windows의 메시지 전달 기능은 고급 로컬 프로시저 호출(advanced local procedure call, ALPC) 기능이라고 한다. 이 기능은 두 프로세스 간 통신에 사용되며 원격 프로시저 호출(RPC)와 유사하지만 Windows에 최적화되어 있다. Windows는 포트 객체를 사용하여 두 프로세스 간의 연결을 설정하고 유지하며 연결 포트(connection port)와 통신 포트(communication port)를 사용한다.
서버 프로세스는 모든 프로세스에서 볼 수 있는 연결 포트를 연다. 클라이언트가 하위 시스템의 서비스를 원할 때 서버의 연결 포트에 핸들(handle)을 열어 연결 요청을 보내고, 서버는 채널(channel)을 만든다음 핸들을 클라이언트에 반환한다. 채널은 클라이언트-서버와 서버-클라이언트 메시지용인 한 쌍의 개인 통신 포트로 구성된다.

위의 그림은 ALPC 구조를 나타낸다.
Windows의 ALPC 기능은 Windows API의 일부가 아니므로 애플리케이션 프로그래머에게 보이지 않는다. 오히려 Windows API를 사용하는 애플리케이션은 표준 RPC를 호출한다. RPC가 동일한 시스템의 프로세스에 호출될 때 ALPC를 통해 간접적으로 처리되고 많은 커널 서비스는 ALPC를 사용하여 클라이언트 프로세스와 통신한다.
파이프(pipe)는 두 프로세스가 통신할 수 있도록 **하는 통로 역할을 한다. 파이프는 UNIX 시스템에서 최초의 IPC 메커니즘이며 가장 간단한 방법이지만 네 가지 문제**를 고려해야 한다.
일반 파이프(Ordinary pipe)는 두 프로세스가 표준 생산자-소비자 방식으로 통신할 수 있도록 한다. 생산자는 파이프의 한쪽 끝(쓰기 끝)에 쓰고 소비자는 다른 쪽 끝(읽기 끝)에서 읽는다. 이 말은 일반 파이프는 단방향 통신만 가능하고, 양방향 통신이 필요하면 두 개의 파이프를 사용해 각 파이프가 다른 방향으로 전송해야 한다.
UNIX 시스템에서 일반 파이프는 pipe(int fd[]) 함수를 사용하여 구성된다. fd[0]은 파이프의 읽기 끝이고 fd[1]은 쓰기 끝이다. UNIX는 특수 유형의 파일로 파이프를 처리하기 때문에 read(), write() 시스템 콜을 사용하여 파이프에 액세스 할 수 있다.
일반 파이프는 파이프를 생성한 프로세스 외부에서는 액세스할 수 없고, 일반적으로 부모 프로세스는 파이프를 만들고 fork()를 통해 만든 자식 프로세스와 통신한다. 파이프는 특수 유형의 파일이기 때문에 자식은 부모로 부터 파이프를 상속받는다.

위의 그림은 파일 설명자와 부모, 자식 프로세스의 관계를 나타낸다.
fd[0]와 fd[1]을 가지고 있다.fd[1]에 모든 내용은 자식의 읽기 끝인 fd[0]에서 읽을 수 있다.예시) UNIX 프로그램에서 부모가 파이프를 생성한 다음 자식 프로세스를 생성하는 코드
#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;
}
pid = fork(); /* 자식 프로세스 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;
}
Windows 시스템의 일반 파이프는 익명 파이프(anonymous pipe)라고 한다. 단방향이며 통신 프로세스 간에 부모-자식 관계를 사용하고, 파이프에 대한 읽기 및 쓰기는 ReadFile() 및 WriteFile() 함수로 수행된다. 또, UNIX 시스템과 달리 프로그래머가 자식 프로세스가 상속할 속성을 지정해야한다.
예시) Windows 프로그램에서 익명 파이프를 만드는 부모 프로세스 코드
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#define BUFFER_SIZE 25
int main(void)
{
HANDLE ReadHandle, WriteHandle;
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;
}
SECURITY_ATTRIBUTES 구조를 초기화하여 핸들을 상속할 수 있도록 한 다음 자식 프로세스의 표준 입력 또는 표준 출력 핸들을 파이프의 읽기 또는 쓰기 핸들로 리디렉션하여 수행한다.예시) Windows 프로그램에서 파이프를 읽는 자식 프로세스 코드
#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;
}
GetStdHandle()을 호출하여 파이프에 대한 읽기 핸들을 얻는다.일반 파이프는 UNIX와 Windows 시스템에서 통신하는 프로세스 간에 부모-자식 관계가 필요하고, 동일한 시스템의 프로세스 간 통신에서만 사용할 수 있다.
일반 파이프는 프로세스가 서로 통신하는 동안에만 존재하지만 명명된 파이프(Named Pipe)는 통신이 양방향일 수 있고, 부모-자식 관계가 필요하지 않으며 통신 프로세스가 완료된 후에도 계속 존재한다. 또, 여러 프로세스가 통신에 사용될 수 있기에 명명된 파이프는 여러 작성자(writer)가 존재한다.
UNIX 시스템의 명명된 파이프는 FIFO라고 하며 파일 시스템에서 일반적인 파일로 나타난다. FIFO는 mkfifo() 시스템 콜로 만들어지고 open(), read(), write(), close() 시스템 콜로 조작된다. FIFO는 양방향 통신을 허용하지만 반이중 전송만 허용되고, 양방향으로 이동해야하는 경우 두 개의 FIFO가 사용된다. 또, 통신 프로세스는 동일한 시스템에서 가능하며, 시스템간 통신이 필요한 경우 소켓을 사용해야 한다.
Windows 시스템의 명명된 파이프는 양뱡향 통신이 허용되며 통신 프로세스는 동일한 시스템과 다른 시스템에서 가능하다. UNIX FIFO는 바이트 지향 데이터만 전송할 수 있지만 Windows 시스템은 바이트나 메시지 지향 데이터를 허용한다. 파이프는 CreateNamedPipe() 함수로 생성되고 클라이언트는 ConnectNamedPipe()를 사용하여 파이프에 연결할 수 있다. 파이프를 통한 통신은 ReadFile() 및 WriteFile() 함수를 사용하여 수행된다.