반응형
더보기
목차
- 프로세스간 통신이란?
- IPC 의 두가지 모델
- Shared Memory
- Message Passing
- IPC 의 구체적인 방법론
- Pipe
- Signal
- Shared Memory
- Message Queue
- Socket
프로세스간 통신이란?
- InterProcess Communication (IPC)
- 서로 다른 프로세스 간에 데이터를 교환하기 위한 메커니즘
- Kernel 이 IPC 를 위한 도구를 제공하며, System Call 을 통해 프로세스들 간 협력을 가능하게 한다.
- 필요성
- 협력하는 프로세스 모델 (Cooperating Process Model) 을 구현하기 위해 IPC 가 반드시 필요함
- 실행 중인 프로세스가 서로 영향을 주고받으며 동작할 수 있음
IPC 의 두가지 모델
1. Shared Memory (공유 메모리)
- 개념
- 프로세스가 동일한 메모리 공간을 공유하여 데이터 교환 (예: 데이터베이스)
- 공유한 Memory 영역에 읽기/쓰기를 통해서 통신 수행
- 빠른 데이터 접근 및 처리 가능
- 응용 프로그램 레벨에서 통신 기능 제공
- 한 번 메모리 영역이 설정되면, 이후 Kernel 의 간섭 없이 직접 통신 가능
- 단점
- 프로세스 간 동기화를 위한 추가적인 메커니즘 필요 (예: 세마포어, Locking)
- 기본 동작
- 주소 공간 일부를 공유 메모리로 설정하여, 프로세스가 바로 접근해 데이터 Read/Write 가능.
- 커널을 거치지 않아 성능이 우수하지만 동기화 문제 해결 필요.
- Process A와 Process B가 하나의 공유 메모리 공간을 통해 데이터를 주고받음.
- 예: 한 프로세스가 쓰면(Write), 다른 프로세스가 읽는(Read) 형태.
2. Message Passing (메시지 교환)
- 개념
- 프로세스 간 메모리 공유 없이 데이터를 주고 받음
- Kernel (중재자) 을 통한 메시지 통신 기능을 제공
- Kernel 이 동기화 제공
- 커널이 동기화를 제공.
- 송신(Send)과 수신(Receive)은 커널에서 동기화를 제공하므로 프로세스에서 추가적인 고려가 필요 없음.
- 고정 길이 메시지, 가변 길이 메시지를 송/수신자 끼리 주고 받음 (예: 클라이언트-서버 통신)
- Pipe, Message Queue, Socket
- 기본 동작
- 커널을 경유하여 메시지를 전송.
- 커널에서 데이터를 버퍼링하고, 문맥 전환과 연관된 오버헤드가 있음.
- Process A가 메시지를 보낼 때(Send), 커널이 이를 전달하고 Process B가 메시지를 받는(Receive) 방식.
방법론
1. Pipe
- 하나의 프로세스가 다른 프로세스로 데이터를 직접 전달하는 방법.
- 데이터는 한 방향으로만 이동 가능(Half Duplex). 양방향 통신을 위해서는 두 개의 Pipe가 필요.
- 한 번에 1:1 의사소통만 가능.
- 순차적으로 데이터를 보냄.
- 용량 제한이 있어 Pipe가 꽉 차면 더 이상 데이터를 보낼 수 없음.
- 예제
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#define MAXLINE 4096
int main(void){
int n, fd[2];
pid_t pid;
char line[MAXLINE];
if (pipe(fd) < 0) {
perror("pipe error");
exit(-1);
}
if ( (pid = fork()) < 0) {
perror("fork error");
exit(-1);
}
else if (pid > 0) {/* parent */
close(fd[0]);
write(fd[1], "hello, world.\n", 14);
}
else { /* child */
close(fd[1]);
n = read(fd[0], line, MAXLINE);
write(STDOUT_FILENO, line, n);
waitpid(pid, NULL, NULL);
}
exit(0);
}
- 동작
- pipe(fd)는 두 개의 파일 디스크립터를 포함한 배열 fd를 생성한다.
- fd[0]: 읽기 전용 디스크립터.
- fd[1]: 쓰기 전용 디스크립터.
- 부모 프로세스 ( fork() > 0 )
- 읽기 디스크립터(fd[0]) 닫기: 부모는 데이터를 쓰기만 하므로 읽기 디스크립터를 닫는다.
- 쓰기 디스크립터(fd[1])로 데이터 쓰기: "hello, world.\n" 문자열을 Pipe에 쓴다.
- 자식 프로세스 ( fork() == 0 )
- 쓰기 디스크립터(fd[1]) 닫기: 자식은 데이터를 읽기만 하므로 쓰기 디스크립터를 닫는다.
- 읽기 디스크립터(fd[0])에서 데이터 읽기: pipe에서 데이터를 읽고, 이를 line 버퍼에 저장한다.
- 표준 출력(STDOUT)으로 데이터 출력: 읽은 데이터를 콘솔에 출력한다.
- pipe(fd)는 두 개의 파일 디스크립터를 포함한 배열 fd를 생성한다.
2. Signal
- 특정 프로세스에게 Kernel을 통해 이벤트를 전달하는 방법.
- 구조 및 특징:
- 송신 프로세스가 신호(Signal)를 발생시키면, 수신 프로세스가 이벤트를 처리. (수신 Process 의 상태와 무관)
ex) Process A가 Signal을 발생시키면, Process B가 Signal 처리 루틴을 실행. - 수신 프로세스는 신호 처리 방법을 지정할 수 있음(예: 단순히 신호를 무시하거나, 핸들러를 통해 처리).
- 비동기적 동작:
- 송신 프로세스가 신호를 보내면, 수신 프로세스가 스케줄링된 이후에 신호를 처리.
- 송신 프로세스가 신호(Signal)를 발생시키면, 수신 프로세스가 이벤트를 처리. (수신 Process 의 상태와 무관)
- 예제
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
void SignalHandlerChild(int signo){
printf("signal handler\n");
fflush(stdout);
}
int main(void){
pid_t pid;
struct sigaction act_child;
act_child.sa_handler = SignalHandlerChild;
act_child.sa_flags = 0;
sigemptyset( &act_child.sa_mask );
sigaction( SIGUSR1, &act_child, 0 );
switch ( pid = fork() ){
case -1:
perror("fork failed ");
exit(-1);
case 0:
sigpause(SIGUSR1);
return 0;
default:
sleep(3);
kill(pid, SIGUSR1);
waitpid(pid, 0, 0);
}
return 0;
}
- 동작
- SignalHandlerChild : 자식 프로세스에서 Signal 을 받으면 실행되는 핸들러 함수
- 시그널을 받았을 때 "Signal Handler" 라는 메시지를 출력하고 출력 버퍼를 플러시하여 즉시 출력이 보이게함
- 자식 프로세스가 SIGUSR1 시그널을 받을 때 실행될 핸들러를 등록한다.
- sigaction() : SIGUSR1 시그널 발생 시 SignalHandlerChild 함수가 호출된다.
- sigemptyset() : 시그널 핸들러가 실행될 때 블록될 시그널 집합을 초기화한다.
- 자식 프로세스 (case 0)
- sigpause (SIGUSR1) : SIGUSR1 시그널을 받을 때까지 대기 상태에 들어간다.
- 시그널을 받으면 등록된 시그널 핸들러가 호출된다.
- 부모 프로세스 (default)
- 3초 동안 대기하여 자식 프로세스가 sigpause() 상태로 진입할 시간을 준다
- kill(pid, SIGUSR1) : 부모 프로세스가 자식 프로세스에게 SIGUSR1 시그널을 보낸다.
- waitpid(pid, 0, 0) : 자식 프로세스의 종료를 기다린다.
- SignalHandlerChild : 자식 프로세스에서 Signal 을 받으면 실행되는 핸들러 함수
3. Shared Memory
- 두 개 이상의 프로세스가 하나의 메모리 영역을 공유하여 통신.
- 특징:
- 메모리에 직접 접근하므로 빠르고 자유롭게 통신 가능.
- 하지만 두 프로세스가 동시에 메모리를 변경하지 않도록 동기화가 필요.
- 구조:
- Shared Memory를 통해 데이터를 교환하며, 각각의 프로세스가 동일한 메모리 영역에 접근 가능.
- 예제
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/wait.h>
#define SHM_SIZE 1024
void ChildRun(int shmid);
int main(int argc, char **argv) {
int shmId, pid;
char *ptrShm;
if ( (shmId = shmget(IPC_PRIVATE, SHM_SIZE, SHM_R | SHM_W)) < 0 ) {
perror("shmget error");
exit(-1);
}
if ( (ptrShm = shmat(shmId, 0, 0)) == (void *)-1 ) {
perror("shmat error");
exit(-1);
}
ptrShm[0] = 11;
ptrShm[1] = 22;
printf("Parent : %d, %d\n", ptrShm[0], ptrShm[1]);
switch ( pid = fork() ){
case 0:
ChildRun(shmId);
return 0;
case -1:
perror("fork error");
exit(-1);
default:
break;
}
waitpid(pid, NULL, 0);
printf("Parent : %d, %d\n", ptrShm[0], ptrShm[1]);
if ( shmdt(ptrShm) < 0 ) {
perror("shmctl error");
exit(-1);
}
if ( shmctl(shmId, IPC_RMID, 0) < 0 ) {
perror("shmctl error");
exit(-1);
}
return 0;
}
void ChildRun(int shmid){
int shmId;
char *ptrShm;
shmId = shmid;
if ( (ptrShm = shmat(shmId, 0, 0)) == (void *)-1 ) {
perror("shmat error");
exit(-1);
}
printf("Child : %d, %d\n", ptrShm[0], ptrShm[1]);
printf("Child : Modify value.\n");
ptrShm[0] = 33;
ptrShm[1] = 44;
if ( shmdt(ptrShm) < 0 ) {
perror("shmctl error");
exit(-1);
}
}
- 동작
- 공유 메모리 생성
- shmget() : 공유 메모리 생성
- IPC_PRIVATE : 특정 프로세스만 공유할 수 있는 메모리 세그멘트 생성
- SHM_R | SHM_W : 읽기, 쓰기 권한 부여
- 공유 메모리 연결
- shmat(shmId, 0, 0) : 공유 메모리 세그먼트를 프로세스의 주소 공간에 연결 (0: 자동 선택)
- 반환된 포인터 ptrShm 을 통해 공유 메모리에 접근
- 부모 프로세스 초기화 및 출력
- 공유 메모리에 초기 값(11, 22)을 설정.
- 설정한 값을 출력.
- fork() 를 이용한 프로세스 분기
- 자식 프로세스 (case 0) : ChildRun() 함수를 호출하여 공유 메모리 데이터를 수정
- 부모 프로세스 (default) : 자식 프로세스의 종료를 기다림
- 자식 프로세스 : 공유 메모리 접근 및 수정
- 자식 프로세스는 공유 메모리를 다시 연결하여 값을 읽고 수정
- 수정된 값:
- ptrShm[0] = 33
- ptrShm[1] = 44
- 공유 메모리 연결 해제 (shmdt)
- 부모 프로세스 : 자식 종료 대기 및 데이터 확인
- 부모 프로세스는 자식 프로세스의 종료를 대기.
- 자식 프로세스가 수정한 데이터를 공유 메모리에서 읽어 출력.
- 공유 메모리 해제
- shmdt() : 공유 메모리 연결을 해제.
- shmctl() : 공유 메모리를 시스템에서 제거(IPC_RMID).
- 공유 메모리 생성
4. Message Queue
- 고정된 크기를 가진 메시지의 연결 리스트(Linked List)를 이용하여 통신하는 방법.
- 특징:
- 메시지 단위로 통신: 메시지의 형식에 따라 통신하려는 프로세스 간의 약속이 필요.
- 공유 가능 : 여러 프로세스가 동시에 메시지 큐를 사용할 수 있지만, 동기화가 필요.
- 사용자 공간에서 메시지를 관리하며, 커널이 메시지 큐를 지원.
ex) Process A가 메시지를 보내면, 다른 프로세스(예: Process B, Process C)가 메시지 큐에서 이를 가져가 읽음.
- 예제
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/wait.h>
typedef struct _MSG {
long type;
char message[256];
} MSG, *PMSG, **PPMSG;
#define MSG_ sizeof(MSG)
int main(void){
pid_t pid;
key_t msg_id;
MSG msg;
if ( -1 == (msg_id = msgget(IPC_PRIVATE, 0660 | IPC_CREAT)) ) {
perror( "msgget failed" );
exit(1);
}
switch ( pid = fork() ) {
case -1:
perror( "fork failed " );
exit(-1);
case 0:
msg.type = 1;
strcpy( msg.message, "Hello, world.");
msgsnd( msg_id, &msg, MSG_-sizeof(long), 0 );
return 0;
default:
waitpid(pid, 0, 0);
memset( &msg, 0, MSG_ );
msgrcv( msg_id, &msg, MSG_-sizeof(long), 1, 0 );
printf("PARENT - message from child : %s\n", msg.message);
fflush(stdout);
}
if ( -1 == msgctl(msg_id, IPC_RMID, 0) ) {
perror( "msgctl failed" );
exit(-1);
}
return 0;
}
- 동작
- 자식 프로세스 : 메시지 전송
- 부모 프로세스 : 메시지 수신차
- 메시지 큐 생성
- mesget()
- 메시지 큐를 생성하거나 기존 큐를 가져온다
- 0660 : 생성된 큐의 접근 권한을 설정(소유자/그룹 읽기/쓰기 가능)
- IPC_CREAT : 큐가 존재하지 않으면 새로 생성
- mesget()
- 프로세스 분기
- 자식 프로세스 : 메시지 전송
- 부모 프로세스 : 메시지 수신
- 메시지 큐 삭제
5. Socket
- 네트워크 통신의 끝점(End-Point)으로, 프로세스 간 데이터를 주고받기 위한 인터페이스.
- 다른 IPC 와 달리 Process 의 위치에 Independent
- 로컬(Local) 및 원격(Remote) 환경에서 모두 사용할 수 있음.
- Local 의 경우 Port 번호로만 식별
- Remote 의 경우 IP 주소 + Port 번호 조합으로 식별
- 로컬(Local) 및 원격(Remote) 환경에서 모두 사용할 수 있음.
- Port 를 이용하여 통신하는데 사용됨
- 운영체제가 제공하는 추상화된 개념
- 특정 프로세스의 소켓을 식별하기 위해 사용
- IP 주소와 결합하여 통신 상대를 고유하게 식별
- Port 번호를 이용하여 통신하려는 상태 Process 의 Socket 를 찾아감
- 연결의 Semantics 를 정할 수 있음 (예: TCP / UDP)
- 특성
- Port 를 사용하기 떄문에 Machine Boundary 와 관계 없음
ex) Port 로 여러 Web Browser 생성- Remote Machine 은 Local Machine 의 Port 만 보임 (Socket 은 보이지 않음)
- Port 를 사용하기 떄문에 Machine Boundary 와 관계 없음
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
void ErrorHandling(char* message);
int main(void){
int port = 9999, acceptState, hServSock, hClntSock;
struct sockaddr_in servAddr, clntAddr;
char buffer[20], message[20] = "message from server";
socklen_t szClntAddr;
pid_t pid;
switch (pid = fork()) {
case -1:
perror("fork failed ");
exit (-1);
case 0:
sleep (1);
hClntSock = socket(AF_INET, SOCK_STREAM, 0);
clntAddr.sin_family = AF_INET;
clntAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
clntAddr.sin_port = htons(port);
szClntAddr = sizeof(clntAddr);
if (connect(hClntSock, (struct sockaddr *)&clntAddr, szClntAddr) < 0){
perror("<Child> Connect error: ");
break;
}
printf("<Child> connect port: %d!\n", port);
recv(hClntSock, buffer, 20, 0);
printf ("<Child> %s\n", buffer);
close(hClntSock);
break;
default:
printf("<Parent> binding port: %d...\n", port);
hServSock = socket(PF_INET, SOCK_STREAM, 0);
if (hServSock == -1)
ErrorHandling("<Parent> socket() error");
memset(&servAddr, 0x00, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
servAddr.sin_port = htons(port);
if (bind(hServSock, (struct sockaddr*)&servAddr, sizeof(servAddr)) < 0 )
ErrorHandling("<Parent> bind() error");
printf("<Parent> listening port: %d...\n", port);
if (listen(hServSock, 1) < 0)
ErrorHandling("<Parent> listen() error");
printf("<Parent> accepting...\n");
while (1) {
szClntAddr = sizeof(clntAddr);
hClntSock = accept(hServSock, (struct
sockaddr*)&clntAddr, &szClntAddr);
if (hClntSock <0) continue;
printf("<Parent> accept port: %d!\n", port);
acceptState = 1; //connected;
if (acceptState == 1) break;
}
send(hClntSock, message, strlen(message)+1, 0);
sleep (1);
close(hServSock);
}
return 0;
}
void ErrorHandling(char* message){
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
- 동작
- 공통 준비 작업
- 포트 번호 : 9999
- 소켓 변수
- hServSock : 서버 소켓 (부모 프로세스)
- hClntSock : 클라이언트 소켓 (자식 프로세스)
- 버퍼
- buffer : 서버에서 클라이언트로 전송된 메시지를 저장할 공간
- message : 서버에서 클라이언트로 보낼 메시지
- fork() 로 부모-자식 프로세스 생성
- 자식 프로세스 : 클라이언트 동작
- 소켓 생성 (IPv4 를 사용하는 TCP 소켓)
- 서버 연결 요청 (IP: 127.0.0.1, Port : 9999)
- 메시지 수신 : 서버에서 전송한 메시지를 읽어와서 출력
- 부모 프로세스 : 서버 동작
- 소켓 생성 (IPv4 기반의 TCP 소켓)
- 포트 바인딩 : port (9999) 에 소켓을 바인딩하여 클라이언트 요청을 받을 준비
- 연결 대기 : 클라이언트 연결 요청을 대기
- 클라이언트 연결 수락
- 메시지 전송 : 클라이언트로 "message from Server" 메시지 전송
- 자식 프로세스 : 클라이언트 동작
- 공통 준비 작업
반응형
'3학년 2학기 학사 > 운영체제' 카테고리의 다른 글
[운영체제] #9. 동기화(2) (0) | 2024.12.01 |
---|---|
[운영체제] #9. 동기화 (1) (0) | 2024.11.27 |
[운영체제] #5. Computer Architecture (0) | 2024.11.24 |
[운영체제] #4. Process (0) | 2024.11.20 |
[운영체제] #3. 운영체제 구조 (2) (0) | 2024.11.19 |