본문 바로가기

Linux & Mac

Inter Process Communication : Message Queue 사용법

반응형

 다양한 소프트웨어를 만들다 보면, 소프트웨어 간 통신이 필요한 경우가 생기기도 합니다. 이때 이용하는 소프트웨어 공학 기술이 Inter Process Communication(IPC)입니다. 이 글에서는 IPC 기법 중 하나인 Message Queue를 이용하는 방법을 다루겠습니다. 

데이터 구조

 메시지 큐는 미리 협약된 key값을 공유하여 운영체제가 관리하는 저장 위치를 이용해 소프트웨어 간 통신을 하는 IPC 기법 입니다. 메시지 큐를 이용할 때는 아래와 같이 협의된 데이터 구조를 활용합니다.

struct myStruct{
    // 반드시 0 이상 이어야 함.
    long myType;
    // 아래 데이터 형식과 크기는 변경 가능.
    char data[1];
};

 구조체를 이용하며, long 형식의 메시지 타입과 데이터로 구성됩니다. 데이터의 형식이나 크기는 자유롭게 변경 가능 하지만, 메시지 타입은 long 형식을 이용해야 하며, 반드시 0 이상의 값을 넣어야 합니다. (메시지 타입이 0 이면 전송되지 않습니다.)

msgget

 메시지 큐를 사용하려면 가장 먼저 메시지 큐를 열고 통신을 위한 아이디를 받아와야 합니다. 이때 msgget함수를 이용합니다. 함수의 입력 변수는 두 가지입니다. 하나는 키이고, 하나는 플래그입니다.

key_t keySnd;

int msqid = msgget(keySnd, IPC_CREAT|0666);

 

 key는 통신을 하는 두 소프트웨어가 공유해야 하는 값입니다. 메시지 큐는 key를 이용해서 각 메시지를 구분합니다. 보내는 쪽과 받는 쪽의 key가 다르면, 당연히 통신이 이루어지지 않겠죠. 두 번째 변수인 플래그는 생성 여부, 접근 권한 등을 지정합니다. 일반적으로는 메시지 큐를 생성하고, 다른 소프트웨어가 접근해야 하니 "IPC_CREAT|0666"을 사용하시고, 상황과 보안 정도에 따라 설정을 변경해 주면 됩니다.

msgsnd

 msgsnd는 메시지를 보내는 함수입니다. 그런데 개념상의 주의점이 있습니다. 이 함수는 메시지를 송신하지만, 수신하는 대상은 상태 소프트웨어가 아닙니다. 수신 대상은 운영체제가 관리하는 메시지 큐의 저장 영역입니다. 메시지 큐는 msgsnd를 호출한 소프트웨어가 있으면 해당 key와 id로 전송 데이터를 저장해 주게 됩니다. 만약 데이터를 읽어 가는 소프트웨어가 없다면 메시지 큐는 저장한 데이터를 계속 유지합니다. 입력 변수는 메시지 큐의 아이디와, 보낼 데이터(구조체)의 주소, 데이터 크기와 플래그입니다.

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

 id는 msgget으로 받은 값이고 msgp는 상기 구조체 형태 변수의 주소입니다. 다음으로 메시지 크기입니다. 이 크기는 구조체 전체가 아니라 데이터의 크기가 됩니다.

 플래그는 특수한 플래그로 IPC_NOWAIT가 있습니다. 메시지 큐는 시스템에서 이용하는 큐입니다. 당연히 크기가 정해져 있습니다. 이 크기는 msgctl함수를 통해 수정이 가능합니다. 어쨌든 메시지 큐의 크기는 유한합니다. 만약 큐가 다 차면 이 함수는 여유 공간이 생길 때까지 대기합니다. IPC_NOWAIT는 큐가 다 찬 경우에 에러를 출력하고 종료하게 하는 플래그입니다.

msgrcv

 msgrcv는 메시지를 받는 함수입니다. msgsnd와 마찬가지로 상대 소프트웨어로부터 데이터를 받는 개념이 아니라, 시스템의 메시지 큐에 저장된 값을 가져옵니다. ( queue이니 당연히 선입선출입니다. ) 함수의 호출은 아래 코드를 따르시면 됩니다.

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

 msgsnd와 같은 형태로 이용하시면 되지만 msgtyp이 추가됩니다. msgtyp은 0인 경우 큐에서 가장 앞에 있는 값을 가져옵니다. msgtyp이 양수로 지정되면 해당 type으로 저장된 값(구조체의 type) 중 가장 앞에 있는 것을 가져오고, 음수 이면 같거나 작은 값들을 가져옵니다. 다대다 혹은 일대다 통신인 경우 유용하게 쓸 수 있겠죠.

msgctl

 msgctl함수는 메시지 큐의 설정을 변경하는 함수입니다. 여러 기능이 있으며, 저 또한 모든 것을 써본 입장이 아니니, 선입관을 드릴 수 있는 설명은 빼겠습니다. 필요할 때 기능을 찾아서 사용하시길 권장합니다.

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

 구조는 위와 같고, 여러 가지 컨트롤을 할 수 있으며, 특히 msgctl(msqId, IPC_RMID, NULL)을 이용해서 사용한 큐를 비울 수 있습니다.

예제

 간단한 예제를 만들어 보았습니다. 먼저 sender입니다.

#include <iostream>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <sys/types.h>

using namespace std;

struct msgStr{
    long mType;
    int data[3];
};

int main(int argc, char ** argv){
    key_t key = 12345;

    int msqid;

    struct msgStr dStr;

    dStr.mType = 1;

    dStr.data[0] = 0;
    dStr.data[1] = 10;
    dStr.data[2] = 20;
    
    msqid = msgget(key, IPC_CREAT|0666);

    for(int i = 0 ; 5 > i ; i++){
	msgsnd(msqid, &dStr, sizeof(dStr.data), 0);

	for(int j = 0 ; 3 > j ; j++){
	    dStr.data[j]++;
	}
    }

    return 0;
}

 이미 각 함수의 사용법들은 위에서 설명하였으니 간단히 구조만 설명하겠습니다. 전송 데이터는 int 형의 크기 3짜리 배열이고, key는 12345입니다. 데이터 타입은 1로 지정합니다. (0이면 전송되지 않음) 배열의 값은 0, 10, 20으로 지정한 후, 5번 전송합니다. 전송 시마다 각 배열의 원소는 1씩 증가합니다.

 다음은 receiver입니다.

#include <iostream>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <sys/types.h>

using namespace std;

struct msgStr{
    long mType;
    int data[3];
};

int main(int argc, char ** argv){
    key_t key = 12345;

    int msqid;

    struct msgStr dStr;

    dStr.mType = 1;

    msqid = msgget(key, IPC_CREAT|0666);
    if(0 > msqid){
	cerr << "not opened message queue key : " << key << endl;;
    }

    for(int j = 0 ; 5 > j ; j++){
	if(sizeof(dStr.data) != msgrcv(msqid, &dStr, sizeof(dStr.data), 0, 0)){
	    cerr << "not recieved msg" << endl;;
	}

	for(int i = 0 ; 3 > i ; i++){
	    cout << dStr.data[i] << " ";
	}
	cout << endl;
    }
    msgctl(msqid, IPC_RMID, NULL);

    return 0;
}

 sender가 보낸 데이터를 받아 출력을 하는 간단한 함수입니다. 종료 직전에는 msgctl함수를 이용해 사용한 큐를 삭제합니다. 이 함수에서는 함수 이용 시 발생하는 에러들에 대해서 에러 처리를 간단하게 해 두었습니다. 소프트웨어의 완성도를 높이고 싶으시면, 해주시는 게 좋겠죠.

 아래 스냅숏은 위 예제들의 구동 결과입니다. 소프트웨어 명에 오타가 있네요. ㅠㅠ 예제 니까 그냥 사용하겠습니다.

 다시 한번 언급하자면 이 통신 기법은 소프트웨어 간의 통신이 아니라 소프트웨어가 메시지 큐를 이용해서 통신하는 기법입니다. 따라서 실시간으로 이루어지는 통신은 아닙니다. 위 예제에서도 같은 터미털로 두 소프트웨어( sender2, receiver2)를 구동시켰습니다. sender2는 reciever2가 구동되지 않았음에도 불구하고, 소프트웨어가 종료됩니다. sender2의 목적은 receiver2에서 데이터를 전송하는 게 아니라 메지지 큐에 데이터를 채우는 것입니다. 해당 기능을 처리한 후 종료한 것이죠.

 receiver2는 메시지 큐를 읽어서 값을 출력해 주었습니다. 역시 sender2가 실행 중이지 않은 상태에서 구동된 것이죠.

 그다음에는 sender2가 두 번 구동됩니다. 간단히 생각해 보면 메시지 큐에는 10개의 데이터가 쌓이겠죠. 이 것을 출력하기 위해 receiver2를 구동시킵니다. 그런데 처음 5개의 데이터를 출력한 후, receiver2는 대기 상태가 되어 버립니다. 이유는 receiver2.cpp 코드에서 소프트웨어 종료 직전에 msgctl을 이용해 큐를 비웠기 때문입니다. 그래서 10개가 차 있던 큐가 5개를 출력한 후 비워지고, 다음에는 출력할 내용이 없어서 대기상태가 된 것입니다. msgctl를 주석 처리한 후 구동시키면 아래 스냅숏과 같이 10개의 값이 출력됩니다.

IPC 상태 확인(ipcs)

 리눅스 시스템을 기반으로 할 때 메시지 큐를 포함 한 IPC의 정보는 ipcs를 이용해 확인할 수 있습니다. 당장 터미널에서 ipcs를 입력하면 아래 스냅숏과 같이 메시지 큐, 공유 메모리, 세마포의 상태를 볼 수 있습니다. 아래 스냅숏을 참조해 주세요.

 ipcs는 스냅숏과 같이 IPC를 이용할 때 사용할 수 있는 메시지 큐, 공유 메모리, 세마포의 상태를 보여줍니다.

 위 스냅숏에서 receiver2는 msgctl을 이용해 큐를 지워줍니다. 그래서 sender2 구동 후 붉은색 상자와 같이 id가 21364736인 데이터가 생기지만 receiver2가 구동된 후 삭제되었습니다.

 그럼 msgctl를 사용하지 않았을 때는 어떻게 될까요? 아래 스냅숏과 같습니다. 

메시지 큐의 내용을 불러 간 후에도 보라색 상자와 같이 생성된 ID의 큐가 남아 있습니다. 이건 의미 없는 리소스 낭비가 되겠죠. 이런 데이터는 삭제되어야 합니다.

ipcrm

 남아 있는 메시지 큐를 삭제하는 명령은 ipcrm입니다. 아래 스냅숏과 같이 -q와 함께 id를 입력해서 삭제해 주시면 됩니다.

이상으로 메시지 큐를 사용하기 위한 방법 및 주요 함수들과 간단한 예제를 설명하는 글을 마치겠습니다.

 

감사합니다.

 

반응형