-
Inter-Process Communication대학/시스템소프트웨어 2022. 10. 23. 01:23
- IPC
프로세스간 통신을 위한 메커니즘으로 두 개의 기본적인 모델이 있다.
- Message System (이 중 하나가 Message queue)
- Shared Memory
- Pipe
pipe는 부모-자식 프로세스간 통신에서 사용하는 메커니즘으로 반이중(단방향) 통신방식을 사용한다.
pipe는 파일로 취급되며, chennel 이라고도 불린다.
pipe() 시스템 콜 함수를 사용하면, 두 개의 파일 지시자가 반환되는데,
파이프의 파일 지시자 0번째 인덱스는 읽기, 1번째 인덱스는 쓰기에 사용된다.
int fd[2]; pipe(fd); if (fork()) { // parent close(fd[0]); write(fd[1], "How are you?", 12); } else { // child close(fd[1]); char buf[100]; read(fd[0], buf, 100); }
위 코드와 그림을 보면 pipe의 작동원리를 알 수 있다.
pipe를 만들고 fork를 통해 부모-자식이 같은 pipe 파일 지시자 배열을 갖도록 한다.
그 후 부모에선 0번(read), 자식에선 1번(write)을 닫아 부모에서 쓰고 자식에서 읽도록 연결해준다.
(그림에서 녹색과 보라색 파이프 파일 지시자를 닫아준다)
그리고 write, read 시스템 콜 함수를 이용해서 pipe 파일 지시자 파일을 읽고 쓰는 식으로 진행하면 된다.
- FIFO (named pipe)
기존 pipe는 부모-자식 프로세스 사이에서만 가능했다면,
FIFO는 어떠한 프로세스 사이에서라도 통신이 가능한 메커니즘으로 반이중 통신방식을 사용한다.
FIFO 역시 파일로 취급된다.
// Sender char s[300]; int fd; mkfifo("myfifo", 0666); fd = open("myfifo", O_WRONLY); while(gets(s)) write(fd, s, strlen(s) // Receiver char s[300]; int fd, num; mkfifo("myfifo", 0666); // Sender에서 이미 만들었기에 -1이 리턴됨 fd = open("myfifo", O_RDONLY); do { num = read(fd, s, 300); s[num] = '\0'; } while (num > 0);
fifo는 하나의 파일을 만드는 식으로 동작하며, fifo 형식의 파일을 만든 후
읽기, 쓰기 전용으로 fifo 파일을 열어 read(), write()로 통신한다.
- Shared Memory
일반적으로 프로세스가 실행될 때, OS는 프로세스에서 0~무한 번지의 가상 메모리를 할당해준다.
따라서, 프로세스 0의 0x0000FFFF 번째 주소와 프로세스 1의 0x0000FFFF 번째 주소는 실제 메모리에서
다른 부분을 차지하고 있다.
실제 메모리에서 한 프로세스는 다른 프로세스가 사용하고 있는 메모리 영역에 합법적으로 접근할 수 없으며,
불법적 접근을 하는 경우에는 이를 heaking 이라고 부른다.
참고로 Data 영역에는 초기화, 초기화 되지 않은 전역변수들이 저장되고,
Heap 영역에는 동적 할당되는 메모리가 저장되며 위로 확장되지만, 무제한으로 확장되지 않는다.
Stack 영역에는 함수들의 프레임이 저장되는데, 각 프레임에는 지역, 전역변수, 인자와 리턴시 주소값 등이 저장된다.
이 영역은 아래로 확장되지만, 역시 무제한으로 확장되지 않는다.
공유 메모리는 물리적 메모리공간에 공유 메모리를 위한 공간을 할당한 후,
프로세스 별로 관리되는 가상 메모리 공간에 공유 메모리 공간의 주소를 보내주어 접근하게 해준다.
공유 메모리는 Segment라고도 불린다.
// Proc 0 - write key_t key; int shmid; char *data; key = ftok("mytoken", 'A'); shmid = shmget(key, 1024, 0644); data = shmat(shmid, (void *)0, 0); for (int i=0; i<256; i++) data[i] = i; shmdt(data); // Proc 1 - read key_t key; int shmid; char *data; key = ftok("mytoken", 'A'); shmid = shmget(key, 1024, 0644); data = shmat(shmid, (void *)0, 0); for (int i=0; i<256; i++) printf("%d", data[i]); shmdt(data);
shmget(key_t key, size_t size, int shmflag) 시스템 콜 함수를 이용해 공유 메모리 공간을 할당할 수 있는데
key 값으로는 시스템 전역적으로 유니크한 키(구분자) 값이 들어가야 한다.
유니크한 키 값은 ftok 시스템 콜 함수로 생성할 수 있으며, IPC_PRIVATE 사용시 부모-자식간에만 사용할 수 있도록 만들 수 있다.
size는 할당할 공유 메모리 공간의 크기이다.
shmflag는 공유 메모리를 만들 때 사용할 플래그나 접근 권한을 지정할 수 있다. (OR 연산으로 둘 다 쓸 수 있다)
반환 값으로는 프로세스 내부에서 사용할 수 있는 id(구분자)가 리턴된다.
shmat 시스템 콜 함수를 이용해 만들어진 공유 메모리에 접근 권한을 획득, 연결할 수 있다.
shmdt 시스템 콜 함수를 사용하면 공유 메모리 접근에 대한 권한을 해제, 연결을 끊을 수 있다.
- 만약 프로세스 1에서 데이터를 읽는 중에 공유 메모리를 detach 한다면?
datach 한 순간부터 유효하지 않은 메모리 공간에 접근하는 꼴이 되어버리기 때문에 에러가 발생한다.
- 만약 프로세스 1번이 프로세스 0번보다 먼저 실행된다면?
원래라면 프로세스 0에서 segment를 생성했겠지만, 이번엔 프로세스 1에서 생성한다. 그러나, 쓰레기 값이 출력된다.
이런 상황을 막기 위해서 프로세스간 동기화(실행 순서 제어)가 필요하다.
여기서 가장 중요한 점은 공유 메모리 공간을 해제하지 않았다는 것이다.
기타 동적 메모리 공간은 프로세스가 종료되면 자동으로 해제가 되지만, 동적 메모리 공간은 프로세스가 종료되지 않아도 해제되지 않는다.
따라서, shmctl(chmid, IPC_RMID, NULL); 시스템 콜 함수로 반드시 해제를 해주자.
단, 하나의 process라도 이 segment를 attach 중이라면, 삭제를 지연시킨다. (모든 프로세스가 detach 될 때 까지)
(참고로, shmctl에 IPC_STAT 플래그를 사용하면 NULL 인자 자리로 segment의 정보가 담긴 shmid_ds 구조체의 포인터가 반환된다)
- Message Queue
메세지 큐는 메세지 객체를 넣고 뺼 수 있는 객체이다. 메일함에 비유된다.
메세지 큐는 파이프와 마찬가지로 반이중(단방향)통신을 지원하는데, n개의 프로세스에서 n개의 프로세스에게로 메세지를 보낼 수 있다.
(하나의 메세지 큐로 다대 다 통신이 가능 - 메모리 효율적이다)
msgget 시스템 콜 함수로 메세지 큐 객체를 생성할 수 있는데, 공유 메모리와 마찬가지로 전역적으로 유니크한 키 값과 플래그를 인자로 받는다. 여기서 유니크한 키 값은 역시 ftok로 생성할 수 있다.
(팁, message queue와 shared memory에서 사용하는 key값이 동일해도 동작에는 지장이 없다)
// Sender int result; int msqid; struct message { long type; char text[20]; } mymsg; mymsg msg; msg.type = 1; strcpy(msg.text, "This is message 1"); result = msgsnd(msqid, (void *)&msg, sizeof(msg.text), 0); // Receiver int result; int msqid; struct message { long type; char text[20]; } mymsg; mymsg *msg; msg = malloc(sizeof(mymsg)); result = msgrcv(msqid, msg, sizeof(msg->text), 1, 0); // result = 17
msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflag) 시스템 콜 함수를 이용해서 메세지 큐에 메세지를 보낼 수 있다.
여기서 중요한 것은 msgp 에는 유저가 정의한 메세지 구조체의 포인터가 들어가야 하는데,
이 구조체의 첫 번째 멤버는 반드시 long 타입의 메세지 타입을 저장할 변수를 멤버로 넣어야 한다.
그리고 msgsz는 sizeof(사용자 정의 구조체) - sizeof(long)으로 정해야 한다.
msgrcv(int msqid, const void *msgp, size_t msgsz, long msgtyp, int msgflag) 시스템 콜 함수를 이용해서
메세지 큐에서 메세지를 읽어올 수 있다.
특정한 메세지 만을 수신하기 위해서 msgtyp 인자가 하나 더 필요한데,
이 인자가 메세지 객체(구조체)의 첫번째 멤버(long 타입의 메세지 타입을 저장하는 변수)와 같다면, 그 메세지 하나를 수신한다.
만약 지정한 msgtyp의 메세지가 메세지 큐에 없다면, 받을 때까지 대기한다. (프로세스가 wait 상태로 전환된다.)
이 함수의 리턴 값은 실제로 수신한 메세지의 크기가 반환된다.
만약 msgtyp위치의 인자를 0으로 넣으면, 메세지 타입과 관계없이 메세지 큐의 가장 앞의 메세지를 가져온다.
만약 0보다 큰 값을 넣으면, 해당하는 메세지 타입 중 메세지 큐의 가장 앞의 메세지를 가져온다. (일반적인 경우)
따라서 프로세스간 통신을 할 때, 특정 프로세스에게로 보내고 싶다면, msgtyp의 값을 pid로 넣어주는 식으로 구현할 수도 있다.
중요한 건, 메세지를 주고 받으려면, 두 프로세스가 동일한 메세지 구조체를 만들어야 하고 메세지 타입도 같아야 한다.
참고로 메세지를 보내는 건 대기 없이 보낼 수 있기에 non-blocking send 라고도 하고,
메세지를 받는 건 대기가 필요할 수 있기에 blocking receive 하고도 부른다.
메세지 큐도 공유 메모리처럼 프로세스 종료시 자동으로 삭제되지 않는다.
따라서 ipcrm 명령어로 수동 삭제하는 방법으로 제거하는 것이 좋다.
또는 아래의 방법으로 자동삭제를 구현하자!
signal(SIGINT, cleanupFunc); signal(SIGSEGV, cleanupFunc); ... cleanupFunc(0); return;
'대학 > 시스템소프트웨어' 카테고리의 다른 글
Synchronization (0) 2022.12.11 Threads (0) 2022.12.11 Timer (0) 2022.10.22 Exceptional Control Flow 2 - Signal (0) 2022.10.22 Exceptional Control Flow 1 - Process (0) 2022.10.22