본문 바로가기
컴퓨터 기초/운영체제 실습

19.메시지큐, 공유 메모리, 세마포어 - ipc

by 인생여희 2020. 7. 21.

#.키와 식별자

시스템 V IPC를 사용하려면 IPC의 객체를 생성해야 하는데, 이를 위해 공통적으로 사용하는 기본 요소가 키와 식별자이다.

 

#.키생성 방법

-키로 IPC_PRIVATE를 지정한다 IPC_PRIVATE를 키로 지정해 생성된 식별자를 서버와 클라이언트 모두 알 수 있게 해야 한다. fork 함수로 생성된 부모-자식 프로세스 간 통신에서 유용하게 사용할 수 있다. 

 

-ftok 함수로 키를 생성한다. ftok 함수는 경로명과 숫자값을 받아서 키를 생성한다. 따라서 서버와 클라이언트가 같은 경로명과 숫자값을 지정하면 공통 식별자를 생성할 수 있다.

 

*상수 :  IPC_PRIVATE

*키 생성 : key_t ftok(const char *path, int id);

*IPC 정보 검색 : ipcs [-aAbciJmopqstZ] [-D mtype]

*IPC 삭제 : ipcrm [-m shmid] [-q msqid] [-s semid] [-M shmkey] [-Q ,sgleu] [-S semkey]

 

키 생성하기 : ftok(3)

#include <sys/ipc.h>

key_t ftok(const char *path, int id);

ftok 함수는 path에 지정한 경로명과 id에 지정한 정수값을 조합해 새로운 키를 생성한다. 경로명은 파일시스템에 존재해야 한다.

 

#.IPC 공통 구조체

IPC에서 공통으로 사용하는 IPC 공통 구조체는 <sys/ipc.h> 파일에 정의되어 있다. 

struct ipc_perm {

    uid_t   uid;    // 구조체의 소유자 ID를 의미한다.

    gid_t   gid;    // 구조체의 소유 그룹 ID를 의미한다.

    uid_t   cuid;   // 구조체를 생성한 사용자 ID를 의미한다.

    gid_t   cgid;   // 구조체를 생성한 그룹 ID를 의미한다.

    mode_t  mode;   // 구조체에 대한 접근 권한을 의미한다.

    uint_t  seq;    // 슬롯의 일련번호를 의미한다.

    key_t   key;    // 키값을 의미한다.

    int     pad[4]; // 향후 사용을 위해 예약되어 있는 영역이다.

}

 

#.시스템 V IPC 정보 검색

시스템 V IPC의 정보를 검색하고 현재 상태를 확인하는 명령은 ipcs이다. ipcs 명령을 실행하는 동안에도 IPC의 상태가 변경될 수 있음.

 

#.시스템 V IPC 정보 삭제

불필요한 IPC 객체를 삭제하려면 ipcrm 명령을 사용

 

#.메시지큐

메시지 큐는 파이프와 유사한데 단, 파이프는 스트림 기반으로 동작하고, 메시지 큐는 메시지(또는 패킷) 단위로 동작한다.

 

*메시지 큐 생성 : int msgget(key_t key, int msgflg);

*메시지 전송 : int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

*메시지 수신 : ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long int msgtyp, int msgflg);

*메시지 제어 : int msgctl(int msqid, int cmd, struct msqid_ds *buf);

 

 

#.메시지큐 수행결과

msgsnd 함수의 수행이 성공하면 msqid_ds 구조체의 항목에서 msg_qnum 값이 1 증가하고, msg_lspid는 msgsnd 함수를 호출한 프로세스의 ID로 설정됩니다. msg_stime은 현재 시각으로 설정됩니다. msgsnd 함수는 수행을 실패하면 -1을 리턴하고 메시지는 전송하지 않습니다.

 

#.공유메모리

메모리의 일부 공간을 두 독립적인 프로세스에서 공유하고, 해당 메모리를 통해 데이터를 주고받을 수 있다.

 

*공유 메모리 생성 : int shmget(key_t key, size_t size, int shmflg);

*공유 메모리 연결 : void *shmat(int shmid, const void *shmaddr, int shmflg);

*공유 메모리 해제 : int shmdt(char *shmaddr);

*공유 메모리 제어 : int shmctl(int shmid, int cmd, struct shmid_ds *buf);

 

 

#.세마포어

세마포어는 프로세스 사이의 동기를 맞추는 기능을 제공해준다. 공유 메모리에 여러 프로세스가 동시에 쓰기를 시도하면 데이터가 손상되는 현상이 발생하므로, 여러 프로세스 사이에서 동작의 순서를 지정해줘야 한다.

 

*세마포어 생성 : int semget(key_t key, int nsems, int semflg);

*세마포어 제어 : int semctl(int semid, int semnum, int cmd, ...);

*세마포어 연산 : int semop(int semid, struct sembuf *sops, size_t nsops);

 


 

#.메시지큐 생성+전송 예제

#include <sys/msg.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
/*
메시지를 담고 있는 메시지 버퍼는 msgbuf 구조체를 사용합니다.
msgbuf 구조체는 <sys/msg.h> 파일에 다음과 같이 정의되어 있습니다.
*/
struct mymsgbuf{
long mtype;
char mtext[80];
};
int main(void){
key_t key;
int msgid;
struct mymsgbuf mesg;
//ftok 함수는 path에 지정한 경로명과 id에 지정한 정수값을 조합해 새로운 키를 생성
key = ftok("keyfile2", 1);
//첫번째 인자인 key에는 IPC_PRIVATE 나 ftok 함수로 생성한 키를 지정
//IPC_CREAT(0001000) : 새로운 키면 식별자를 새로 생성한다.
//IPC_EXCL(0002000) : 이미 존재하는 키면 오류가 발생한다.
msgid = msgget(key, IPC_CREAT|0644);
if (msgid == -1) {
perror("msgget");
exit(1);
}
// 전송할 메시지 버퍼를 설정한다.
mesg.mtype = 1; // 메시지 유형을 1로 정의
strcpy(mesg.mtext, "Message Q Test\n"); // 메시지 버퍼의 배열에 문자열을 복사.
// msgsnd 함수를 사용해 메시지를 전송한다.
if (msgsnd(msgid, (void *)&mesg, 80, IPC_NOWAIT) == -1) {
perror("msgsnd");
exit(1);
}
return 0;
}
view raw sharememory1.c hosted with ❤ by GitHub

결과

메시지큐가 생성되었다.

 

 

#.메시지큐 받기 예제

#include <sys/msg.h>
#include <stdlib.h>
#include <stdio.h>
typedef struct mymsgbuf {
long mtype;
char mtext[80];
}mymsgbuf;
int main(void) {
mymsgbuf inmsg;
key_t key;
int msgid, len;
// msgsnd 예제와 같은 경로명과 정수값을 사용해 키를 생성한다.
key = ftok("keyfile", 1);
// msgget 함수의 두번째 인자를 0으로 지정해 기존 메시지 큐의 식별자를 리턴하게 한다.
if((msgid = msgget(key, 0)) < 0) {
perror("msgget");
exit(1);
}
// msgrcv 함수를 사용해 메시지를 읽어온다.
// 버퍼의 크기는 80바이트로 지정하고, 큐가 비었을 경우 기다리도록 지정한다.
len = msgrcv(msgid, &inmsg, 80, 0, 0);
printf("Received Msg = %s, Len=%d\n", inmsg.mtext, len);
return 0;
}
/*
msgrcv 함수는 메시지 큐로 메시지를 수신하는 데 사용합니다.
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long int msgtyp, int msgflg);
msqid: msgget 함수로 생성한 메시지 큐 식별자
msgp: 메시지를 담고 있는 메시지 버퍼의 주소
msgsz: 메시지 버퍼의 크기
msgtyp: 읽어올 메시지의 유형
msgflg: 블록 모드(0)/비블록 모드(IPC_NOWAIT)
*/
view raw sharememory2.c hosted with ❤ by GitHub

결과

위의 예제에서 메시지큐에 저장한 Message Q Test라는 글자를 받았다.

 

 

#.메시지큐 제거 예제

#include <sys/msg.h>
#include <stdlib.h>
#include <stdio.h>
/*
msgctl 함수는 메시지 큐를 제거하거나 상태 정보를 설정하고 읽어오는 메시지 큐에 대한 제어 기능을 수행합니다.
#include <sys/msg.h>
int msgctl(int msgid, int cmd, struct msqid_ds *buf);
msqid: msgget 함수로 생성한 메시지 큐 식별자
cmd: 수행할 제어 기능
buf: 제어 기능에 사용되는 메시지 큐 구조체의 주소
msgctl 함수는 msqid로 지정한 메시지 큐에서 cmd에 지정한 제어를 수행합니다. buf는 cmd의 종류에 따라 제어값을 지정하거나 읽어오는 데 사용합니다. cmd에는 다음 중 하나를 지정합니다.
IPC_RMID : msqid로 지정한 메시지 큐를 제거하고, 관련 데이터 구조체를 제거한다.
IPC_SET : 메시지 큐의 정보 중 msg_perm.uid, msg_perm.gid, msg_perm.mode, msg_qbytes 값을 세 번째 인자로 지정한 값으로 바꿉니다. 이 명령은 root 권한이 있거나 유효 사용자 ID인 경우만 사용할 수 있습니다. msg_qbytes는 root 권한이 있어야 변경할 수 있습니다.
IPC_STAT: 현재 메시지 큐의 정보를 buf로 지정한 메모리에 저장합니다.
*/
int main(void) {
key_t key;
int msgid;
// 이전 예제와 같은 키를 생성해 메시지 큐 식별자를 리턴받는다.
key = ftok("keyfile", 1);
msgid = msgget(key, IPC_CREAT|0644);
if(msgid == -1) {
perror("msgget");
exit(1);
}
printf("Before IPC_RMID\n");
//명령어 실행
system("ipcs -q");
// 메시지 큐 식별자를 IPC_RMID 명령으로 제거한다.
msgctl(msgid, IPC_RMID, (struct msqid_ds *)NULL);
printf("After IPC_RMID\n");
//명령어 실행
system("ipcs -q");
return 0;
}
view raw sharememory3.c hosted with ❤ by GitHub

결과

아래 메시지큐가 비어있는것을 알 수 있다.

 

 

#.공유메모리 생성 예제

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <stdio.h>
//공유할 메모리의 크기가 1KB인 공유 메모리를 생성하는 예제
/*
// key: IPC_PRIVATE 또는 ftok 함수로 생성한 키
// size: 공유할 메모리의 크기
// shmflg: 공유 메모리의 속성을 지정하는 플래그
int shmget(key_t key, size_t size, int shmflg);
shmget 함수는 인자로 키, 공유할 메모리의 크기, 플래그를 받아 공유 메모리 식별자를 리턴합니다.
key에는 IPC_PRIVATE나 ftok 함수로 생성한 키를 지정합니다.
size에는 공유할 메모리의 크기를 지정합니다.
이미 공유된 메모리의 식별자를 읽어오는 것이라면 무시합니다.
shmflg에는 플래그와 접근 권한을 지정합니다.
사용할 수 있는 플래그는 msgget 함수와 마찬가지로 IPC_CREAT와 IPC_EXCL입니다.
*/
int main(void) {
key_t key;
int shmid;
key = ftok("shmfile" , 1); //키 생성
shmid = shmget(key, 1024 , IPC_CREAT|0644); //1kb 공유메모리 생성
if (shmid == -1) {
perror("shmget");
exit(1);
}
return 0;
}
view raw sharememory4.c hosted with ❤ by GitHub

결과

공유메모리가 생성되었다.

 

 

#.부모 프로세스와 자식 프로세스 사이에서 공유 메모리를 사용해 데이터를 주고 받는 예제

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
//부모 프로세스와 자식 프로세스 사이에서 공유 메모리를 사용해 데이터를 주고 받기.
/*
*공유메모리 연결
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid: shmget 함수로 생성한 공유 메모리 식별자
shmaddr: 공유 메모리를 연결할 주소
shmflg: 공유 메모리에 대한 읽기/쓰기 권한
*/
int main(void) {
int shmid , i;
char *shmaddr , *shmaddr2;
//키를 IPC_PRIVATE로 지정해 공유 메모리를 20바이트 크기로 생성.
shmid = shmget(IPC_PRIVATE, 20, IPC_CREAT|0644);
if (shmid == -1) {
perror("shmget");
exit(1);
}
//fork 함수로 자식 프로세스 생성
switch (fork()) {
case -1:
perror("fork");
exit(1);
break;
case 0 : //자식 프로세스 수행영역
//공유 메모리 연결
shmaddr = (char *)shmat(shmid, (char *)NULL,0);
printf("child Process =====\n");
//공유메모리에 'a' - 'j' 작성
for (i = 0; i < 10; i++) {
shmaddr[i] = 'a' + i;
}
shmdt((char*)shmaddr); //공유메모리 해제
exit(0);
break;
default: //부모영역
wait(0); //자식이 종료하길 대기
//공유메모리 연결
shmaddr2 = (char *)shmat(shmid, (char *)NULL, 0);
printf("Parent Process =====\n");
for (i = 0; i < 10; i++) {
printf("%c" , shmaddr2[i]);
}
printf("\n");
// sleep을 사용하는 이유
// ipcs 명령으로 공유 메모리의 상태를 확인하기 위해서. 물론 메시지 큐에서처럼 system 함수를
// 사용할 수도 있다. 다음처럼 공유 메모리가 생겼다가 프로세스가 종료된 후에는 삭제되는 것을 알수 있다.
sleep(5);
//공유메모리 해제
shmdt((char *)shmaddr2);
//공유 메모리 제거
shmctl(shmid, IPC_RMID, (struct shmid_ds *)NULL);
break;
}
return 0;
}
/*
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid: shmget 함수로 생성한 공유 메모리 식별자
cmd: 수행할 제어 기능
buf: 제어 기능에 사용되는 공유 메모리 구조체의 구조
*설명
shmctl 함수는 shmid가 가리키는 공유 메모리에 cmd로 지정한 제어 기능을 수행합니다.
buf에는 제어 기능에 따라 사용되는 공유 메모리 구조체의 주소를 지정합니다.
cmd에 지정할 수 있는 값은 다음과 같습니다.
IPC_RMID: shmid로 지정한 공유 메모리를 제거하고, 관련 데이터 구조체를 제거한다.
IPC_SET: 공유 메모리의 정보 내용 중 shm_perm.uid, shm_perm.gid, shm_perm.mode 값을 세번째 인자로 지정한 값으로 바꾼다. 이 명령은 root 권한이 있거나 유효 사용자 ID인 경우만 사용할 수 있다.
IPC_STAT: 현재 공유 메모리의 정보를 buf로 지정한 메모리에 저장한다.
SHM_LOCK: 공유 메모리 세그먼트를 잠근다.
SHM_UNLOCK: 공유 메모리 세그먼트의 잠금을 해제한다.
*/
view raw sharememory5.c hosted with ❤ by GitHub

결과

자식프로세스에서 공유메모리에 쓰고, 부모프로세스에서 읽었다.

 

 

#.독립적인 프로세스 사이에서 공유 메모리를 사용해 데이터를 주고 받는 예제

-서버

#include <sys/types.h>
#include <sys/mman.h>
#include <signal.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
/*
서버 역할을 하는 server 에서는 공유 메모리를 생성하고,
client가 공유 메모리에 데이터를 기록하고 시그널을 보낼 때까지 기다립니다.
시그널을 받으면 공유 메모리에서 데이터를 읽어오고, 응답 데이터를 공유 메모리에 기록합니다.
*/
void handler(int dummy){
printf("handler called \n");
}
int main(void){
key_t key;
int shmid;
void *shmaddr;
char buf[1024];
sigset_t mask;
//공유메모리 생성
key = ftok("shmfile" , 1);
shmid = shmget(key, 1024, IPC_CREAT|0666);
//SIGUSR1 시그널을 받을때까지 기다리도록 한다.
sigfillset(&mask); //집합 변수에 모든 시그널 정보를 넣었음
sigdelset(&mask, SIGUSR1); //시그널 제거
sigset(SIGUSR1 , handler); //시그널 처리 함수를 변경
printf("Listener wait for Talker \n");
sigsuspend(&mask); //시그널을 BLOCK시킴과 동시에 대기합니다
//시그널을 받으면 공유메모리를 연결
//client 가 공유 메모리에 저장한 데이터를 읽어서 출력한다.
printf("Listner Start =====\n");
//공유 메모리 연결
shmaddr = shmat(shmid, NULL , 0);
//공유 메모리 내용을 buf에 복사
strcpy(buf , shmaddr);
printf("Listener recieved : %s\n" , buf);
//공유 메모리에 응답데이터 저장
strcpy(shmaddr , "have nice day \n");
//공유 메모리의 연결을 끊기 전에 sleep 함수를 실행하는 이유는
//client가 ipcs를 실행할 시간을 주기 위해서다.
sleep(3);
//공유메모리 해제
shmdt(shmaddr);
return 0;
}
view raw server.c hosted with ❤ by GitHub

-클라이언트

#include <sys/types.h>
#include <signal.h>
#include <sys/mman.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
/*
클라이언트 역할을 하는 client에서는 server가 생성한 공유 메모리의 식별자를 읽어서
공유 메모리를 연결하고 데이터를 기록한 후 listener에 시그널을 보냅니다.
잠시 기다렸다가 server가 기록한 데이터를 읽어서 출력하고 공유 메모리를 해제한 후 삭제합니다.
*/
int main(int argc, char *argv[]){
key_t key;
int shmid;
void *shmaddr;
char buf[1024];
//client는 server와 같은 파일과 정수값을 사용해 키를 생성한다.
key = ftok("shmfile", 1);
//키값으로 server가 만든 공유 메모리의 식별자를 읽어온다.
shmid = shmget(key, 1024, 0);
//공유 메모리와 연결하고 해당 메모리에 인삿말을 복사한다.
shmaddr = shmat(shmid, NULL,0);
strcpy(shmaddr , "hello, i am talker\n");
//명령행 인자로 받은 server의 pid를 지정하고 SIGUSR1 시그널을 보낸다.
kill(atoi(argv[1]) , SIGUSR1);
//sleep 함수를 수행해 잠시 기다렸다가 공유 메모리에서 listener가 보낸 응답을 읽어 출력한다.
sleep(2);
strcpy(buf, shmaddr);
printf("Listener said : %s\n" , buf);
//현재 공유 메모리의 상태 정보를 검색한 후, 공유 메모리 연결을 해제하고
system("ipcs -m");
shmdt(shmaddr);
//공유 메모리를 제거한다.
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
view raw client.c hosted with ❤ by GitHub

결과

 

#.세마포어 예제

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <stdio.h>
#define MAX_THREAD 2
int count = 0;
void *myThreadFunc(void *data);
/*
sembuf 구조체는 <sys/sem.h> 파일에 정의되어 있습니다.
struct sembuf {
ushort_t sem_num; // 세마포어 번호를 의미
short sem_op; // 세마포어 연산을 의미
// 연산을 위한 플래그로, IPC_NOWAIT 또는 SEM_UNDO를 지정한다.
// SEM_UNDO: 프로세스가 비정상적으로 갑자기 종료할 때
// 세마포어 동작을 취소한다.
short sem_flg;
};
*/
struct sembuf mysem_open = {0, -1, SEM_UNDO};
struct sembuf mysem_close = {0, 1, SEM_UNDO};
union snum
{
int val;
};
static int semid;
int main(int argc, char **argv)
{
int thr_id;
int status;
int i;
union snum s_union;
pthread_t pt[MAX_THREAD]; //스레드 2개
//세마포어 생성
/*
세마포어는 커널이 관리한다.
semget을 이용해서 커널에 세마포어의 생성 혹은 기존에 만들어진 세마포어의 접근을 요청할 수 있다.
semget의 인자는 다음과 같다.
key : 세마포어에 접근하기 위한 key 값
nsems : 세마포어 계수로 접근제하하려는 자원의 수
semflg : 세마포어 동작제어를 위한 옵션
*/
semid = semget(2345, 1, 0600|IPC_CREAT);
if(semid == -1)
{
perror("semget error");
return 1;
}
s_union.val = 1;
//세마포어의 세부값 제어
//SETVAL: 세마포어의 semval 값을 arg.val로 설정한다.
/*
네 번째 인자는 제어 명령에 따른 선택 사항입니다.
네 번째 항목이 필요할 경우 다음과 같은 공용체(union)을 사용합니다.
이 공용체는 프로그램에서 명시적으로 선언하고 사용해야 합니다.
union semun {
int val;
struct semid_ds *buf;
ushort_t *array;
} arg;
*/
if(semctl(semid, 0, SETVAL, s_union) == -1)
{
return 1;
}
for(i = 0; i < MAX_THREAD; i++)
{
//스레드 생성
thr_id = pthread_create(&pt[i], NULL, myThreadFunc, (void *)&i);
//성공할경우 쓰레드식별자인 thread에 쓰레드 식별번호를 저장하고, 0을 리턴한다. 실패했을경우 0 이 아닌 에러코드 값을 리턴한다.
if(thr_id < 0)
{
perror("Thread Create Error");
return 1;
}
sleep(1);
}
for(i = 0; i < MAX_THREAD; i++)
{
//pthread_join 는 실별번호 th 로 시작된 쓰레드가 종료되는걸 기다린다.
pthread_join(pt[i], NULL);
/*
joinable 쓰레드가 종료되면, 종료된다고 하더라도 즉시 메모리 자원등이 해제 되지 않는다.
pthread_join 함수를 만나야지만 자원이 해제된다.
그럼으로 모든 joinable 쓰레드에 대해서는 반드시 pthread_join 을 호출해주어야 한다.
그렇지 않을경우 메모리 누수가 발생할것이다.
*/
}
}
void *myThreadFunc(void *data)
{
int thread_num = *(int *)data;
int lnum;
printf("Thread Create : %d\n", thread_num);
while(1)
{
/*
*잠금과 해제와 같은 세마포어 연산을 수행하려면 semop 함수를 사용합니다.
*semop 함수는 semid가 가리키는 세마포어에 크기가 nsops인 sembuf 구조체로 지정한 연산을 실행합니다
sedmid: semget 함수로 생성한 세마포어 식별자
sops: sembuf 구조체의 주소
nsops: sops가 가리키는 구조체의 크기
*/
semop(semid, &mysem_open, 1);
lnum = count;
sleep(1);
lnum = lnum+1;
count = lnum;
printf("[스레드%d번] count : %d\n", thread_num, count);
semop(semid, &mysem_close, 1);
}
}
view raw semaphoer.c hosted with ❤ by GitHub

결과

각각의 스레드가 공유자원에 한번씩 순차적으로 접근을 한다.  semop 함수를 주석처리해서 실행하면 동기화가 되지 않는 현상이 나타난다.

 

 

 

 

 

 

출처: 유닉스 프로그래밍 (한빛미디어) , joinC