메시지큐, 공유메모리 등 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

메시지 큐

IPC기법 중 하나인 메시지큐는 Data Structure 중 큐를 사용합니다. 기본적으로는 먼저온 메시지가 먼저 꺼내어집니다. 메시지큐의 msgtype에 따라 특정 메시지 중 가장 먼저들어온 메시지도 받아올 수 있습니다. 이 메시지는 커널에서 보관하고 있으니 프로세스가 종료되어도 사라지지 않습니다.

메시지 큐의 용량이 허용하는 한 메시지는 계속 큐에 쌓일 수 있습니다. 메시지를 얻어오는 쪽은 가장 메시지를 읽고 메시지큐에서 그 메시지를 삭제합니다.

 

메시지 큐에는 두가지의 종류가 있습니다.

- System V의 Message Queue

- POSIX의 Message Queue

mq_open, mq_send, mq_receive와 같은 함수를 사용하는 POSIX에서 메시지큐 활용방법을 알아보시려면 아래의 포스팅을 참고하시면 됩니다.

https://reakwon.tistory.com/209

 

[리눅스] POSIX 메시지 큐 사용 방법 - 예제 코드

Message Queue 프로세스 간 통신 방식인 IPC기법 중 하나인 메시지 큐는 두 가지 사용 방법이 있습니다. msgget, msgsend, msgrecv와 같은 함수를 사용하는 방식인 System V 방식의 메시지큐, 그리고 지금 알아

reakwon.tistory.com

이번 포스팅에서는 System V의 Message Queue를 리눅스에서 어떻게 이용할 수 있는지 알아보도록 합시다.

 

메시지 큐 관련 시스템 콜

메시지큐를 사용하기 위해서는 3개의 헤더파일이 필요합니다.

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

 

1) msgget : System V의 메시지 큐 id를 얻어옵니다.

 int msgget(key_t key, int msgflg);

 

key : 메시지큐를 얻어올 때 사용하는 고유 key값입니다.

msgflg : flag에는 2가지가 있는데요. IPC_CREAT과 IPC_EXCL입니다.

 - IPC_CREAT : 메시지큐가 없으면 새로 생성합니다.

 - IPC_EXCL : IPC_CREAT과 같이 사용하는 flag인데, 만약 해당 메시지큐가 존재하면 msgget은 오류를 반환합니다.

 

2) msgsnd : 메시지를 보냅니다. 

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

 

msqid : 메시지 큐의 id입니다.

msgp : msgp는 void*이나 우리는 구조체 형식으로 아래와 같이 정의해주어야합니다. 

struct msgbuf {
       long mtype;       /* message type, must be > 0 */
       char mtext[1];    /* message data */
};

 

- mtype : 메시지의 타입을 나타냅니다. 이 값은 0보다 커야한다고 주석이 말해주고 있군요. 메시지큐의 msgbuf를 정의할때 long형의 mtype을 반드시 명시해주어야합니다. 

- mtext : mtext은 실제 메시지 큐에 보낼 데이터를 말합니다. mtext와 같이 배열일 수도 있고, 구조체일 수도 있습니다. 

msgsz : msgsz는 메시지 큐에 전송할 데이터의 사이즈를 의미하는데, 위의 msgbuf의 mtype멤버를 제외한 실데이터의 크기를 전달해야합니다. 

msgflg : 큐의 공간이 없을때 msgsnd의 동작은 blocking입니다. 즉, msgsnd에서 큐의 공간이 생겨날때까지 기다리는 것이지요. 여기서 IPC_NOWAIT을 사용한다면 msgsnd는 blocking되지 않고 실패합니다.

msgsnd가 성공적으로 동작하면 msqid_ds라는 구조체의 필드의 값이 변경됩니다. sys/msg.h에 이 구조체가 정의되어있습니다.

 

struct msqid_ds {
       struct ipc_perm msg_perm;     /* Ownership and permissions */
       time_t          msg_stime;    /* Time of last msgsnd(2) */
       time_t          msg_rtime;    /* Time of last msgrcv(2) */
       time_t          msg_ctime;    /* Time of last change */
       unsigned long   __msg_cbytes; /* Current number of bytes in
                                                queue (nonstandard) */
       msgqnum_t       msg_qnum;     /* Current number of messages
                                                in queue */
       msglen_t        msg_qbytes;   /* Maximum number of bytes
                                                allowed in queue */
       pid_t           msg_lspid;    /* PID of last msgsnd(2) */
       pid_t           msg_lrpid;    /* PID of last msgrcv(2) */
};

이 구조체는 메시지큐의 메타데이터같은 것인데 msgsnd를 성공적으로 호출하고 나면 아래와 같이 변경됩니다.

- msg_lspid는 호출된 process id로 변경

- msg_qnum의 값 1 증가

- msg_stime을 현재 시간으로 설정

 

3) msgrcv : msgsnd를 했다면 받는 시스템 콜이 있어야겠지요. msgrcv가 그 역할을 합니다. 메시지큐 id의 메시지를 하나 읽고 그 메시지를 큐에서 제거합니다.

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

 

msqid : 다 아시겠지만 메시지큐 id입니다.

msgp : 읽어들인 메시지를 msgp가 가리키는 주소에 위치시킵니다. 쉽게말해 읽어온 메시지입니다.

msgsz : 메시지를 읽을 크기(더 정확히는 msgbuf의 text 크기입니다.)인데 만약 읽어들인 메시지가 지정된 크기보다 크다면 msgflg에 따라 동작이 결정됩니다. 만약 msgflg가 MSG_NOERROR라면 메시지를 읽어들이나 잘려서 읽히게 됩니다. MSG_NOERROR가 명시되어있지 않다면 메시지를 읽어오지 않고 msgrcv 시스템콜은 -1을 반환합니다.

msgtyp : 0, >0, <0으로 동작이 나뉩니다.

msgtyp == 0 큐의 첫번째 메시지를 읽어옵니다.
msgtyp > 0 그 값과 동일한 메시지 타입을 갖는 메시지를 반환합니다. 
msgtyp < 0 msgtyp의 절대값 이하의 가장 작은 메시지를 읽어옵니다.

msgflg : 4개 정도의 flag가 있습니다. 

- IPC_NOWAIT

- MSG_COPY : 리눅스 3.8이상부터 지원합니다.

- MSG_EXCEPT

- MSG_NOERROR

 

4) msgctl : 메시지큐를 제어하기 위한 시스템 콜입니다. 

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

msqid : 다들 아시죠? 메시지 큐 id입니다.

cmd : 제어할 command입니다. 몇개 있는데 3개만 보도록 하겠습니다.

- IPC_STAT : buf로 메시지큐 정보를 읽어옵니다. 

- IPC_SET : buf로 메시지큐 정보를 설정합니다.

- IPC_RMID : 메시지큐를 지웁니다.

buf : 아까 위에서 보았던 구조체네요. cmd로 IPC_STAT과 IPC_SET을 사용할때 전달해주면 되고 

딱히 필요없으면 NULL을 전달합니다.

 

이제 메시지큐를 통한 예제를 볼까요?

아래의 예제는 sender에서 어떤 사람의 나이와 이름을 receiver에게 전달하는 메시지 큐 예제입니다. sender에서는 메시지큐의 정보 또한 보여주고 있습니다. 메시지 큐에 있는 message라는 구조체는 sender와 receiver가 사용하고 있으므로 헤더파일 msg_data.h를 두어 같이 사용합니다.

 

예제는 너무 간단하기 때문에 따로 설명할 필요는 없고 한번 따라 해보면 다 이해하실거에요.

 

msg_data.h

struct real_data{
        short age;
        char name[16];
};
struct message{
        long msg_type;
        struct real_data data;
};

 

sender.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/msg.h>
#include <stdlib.h>
#include <string.h>
#include "msg_data.h"


void printMsgInfo(int msqid){

        struct msqid_ds m_stat;
        printf("========== messege queue info =============\n");
        if(msgctl(msqid,IPC_STAT,&m_stat)==-1){
                printf("msgctl failed");
                exit(0);
        }
        printf(" message queue info \n");
        printf(" msg_lspid : %d\n",m_stat.msg_lspid);
        printf(" msg_qnum : %d\n",m_stat.msg_qnum);
        printf(" msg_stime : %d\n",m_stat.msg_stime);

        printf("========== messege queue info end =============\n");
}
int main(){
        key_t key=12345;
        int msqid;

        struct message msg;
        msg.msg_type=1;
        msg.data.age=80;
        strcpy(msg.data.name,"REAKWON");

        //msqid를 얻어옴.
        if((msqid=msgget(key,IPC_CREAT|0666))==-1){
                printf("msgget failed\n");
                exit(0);
        }

        //메시지 보내기 전 msqid_ds를 한번 보자.
        printMsgInfo(msqid);

        //메시지를 보낸다.
        if(msgsnd(msqid,&msg,sizeof(struct real_data),0)==-1){
                printf("msgsnd failed\n");
                exit(0);
        }

        printf("message sent\n");
        //메시지 보낸 후  msqid_ds를 한번 보자.
        printMsgInfo(msqid);
}

 

receiver.c 

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/msg.h>
#include <stdlib.h>
#include <string.h>
#include "msg_data.h"

int main(){
        key_t key=12345;
        int msqid;
        struct message msg;

        //받아오는 쪽의 msqid얻어오고
        if((msqid=msgget(key,IPC_CREAT|0666))==-1){
                printf("msgget failed\n");
                exit(0);
        }
        //메시지를 받는다.
        if(msgrcv(msqid,&msg,sizeof(struct real_data),0,0)==-1){
                printf("msgrcv failed\n");
                exit(0);
        }

        printf("name : %s, age :%d\n",msg.data.name,msg.data.age);

        //이후 메시지 큐를 지운다.
        if(msgctl(msqid,IPC_RMID,NULL)==-1){
                printf("msgctl failed\n");
                exit(0);
        }
}

 

두개의 c파일을 컴파일 해줍시다.

# gcc sender.c -o sender
# gcc receiver.c -o receiver

 

이제 sender부터 실행하도록 합시다.

# ./sender
========== messege queue info =============
 message queue info
 msg_lspid : 0
 msg_qnum : 0
 msg_stime : 0
========== messege queue info end =============
message sent
========== messege queue info =============
 message queue info
 msg_lspid : 5129
 msg_qnum : 1
 msg_stime : 1586521322
========== messege queue info end =============

 

메시지 큐의 정보가 변경된 것을 알 수있네요. 메시지가 성공적으로 보내졌나봅니다.

ipcs를 구경해볼까요?

# ipcs -q

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages
0x00003039 5          root       666        18

 

0x3039는 우리가 정한 key 12345입니다. sender 프로그램이 끝나고 메시지 큐는 아직 남아있습니다. 

이제 receiver를 실행해보도록 하지요.

# ./receiver
name : REAKWON, age :80

 

음, 잘 읽히네요. 한번더 receiver를 실행하면 아무런 동작을 하지 않습니다. 메시지큐에 아무것도 없기 때문입니다. ipcs를 들여다보면 메시지큐가 삭제되었네요. 그 이유는 receiver 코드를 보면 msgctl에서 cmd를 IPC_RMID를 주어 제거했기 때문입니다. 

# ipcs -q

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages

 

이상으로 메시지큐와 관련한 포스팅을 마치도록 하겠습니다. 긴글 읽어주셔서 감사합니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

공유 메모리, 메시지큐 등 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

공유메모리(Shared Memory)

프로세스에서 메모리는 해당 프로세스만이 사용하는게 일반적입니다. 메모리에는 명령어, 지역 변수, 동적 변수, 전역 변수와 같이 데이터가 존재하는데 그 프로세스만 접근할 수 있고 변경가능합니다. 일반적으로 이렇다는 건데, 아주 가끔 이 데이터가 다른 프로세스에서 쓰일 수 있도록 만들 수도 있습니다. 그게 바로 공유메모리라는 IPC기법입니다.

서로 다른 프로세스가 특정 메모리를 공유하면 데이터를 더 빠르게 접근할 수 있기 때문에 프로그램을 더 효율적으로 만들 수 있습니다. 이해도 직관적으로 할 수 있기 때문에 코드도 그렇게 어렵지가 않습니다.

 

관련 함수

공유메모리를 사용하기 위해서 우리가 필요한 헤더파일은 아래와 같이 2개입니다.

 #include <sys/shm.h> 
 #include <sys/ipc.h>

 

1) shmget : 인자로 전달된 key의 값으로 공유메모리를 얻고 공유메모리 조각(shared memory segment)의 id를 돌려줍니다. 

int shmget(key_t key, size_t size, int shmflg);

key : 공유메모리를 할당할때 사용하는 고유 key값입니다.

size : 메모리의 최소 size를 의미합니다. 새로운 공유메모리를 할당받는다면 size를 명시하고 이미 존재하는 메모리면 0을 주면 됩니다.

shmflg : IPC_CREATIPC_EXCL 두 가지가 존재합니다.

- IPC_CREAT : 새로운 메모리 세그먼트는 만듭니다. 이 flag를 사용하지 않는다면 shmget은 명시된 key와 연관된 찾고 접근할 수 있는 권한이 있는지 확인합니다.

 

- IPC_EXCL : IPC_CREAT과 함께쓰는 플래그로 만약 메모리 세그먼트가 존재하면 shmget은 실패하게 됩니다.

IPC_EXCL을 사용하는 경우는 우선 공유메모리가 있는지 확인 후 없으면 IPC_CREAT을 통해 할당받으라는 뜻입니다. 이렇게 공유메모리가 오염되는 것을 방지할 수 있습니다.

 

2) shmat : 공유메모리를 얻었으면 메모리의 위치에 이 프로세스를 묶는(attach) 시스템 콜입니다.

void *shmat(int shmid, const void *shmaddr, int shmflg);

shmid : 공유메모리의 id를 의미합니다. shmget을 통해 얻어올 수 있습니다.

shmaddr

-NULL(0)일 경우 : 커널에서 적절한 주소를 반환하여 줍니다.

-NULL이 아닐 경우 : shmflg로 SHM_RND일때, 그 주소와 attach할 가장 가까운 주소를 반환합니다.

 

정상적으로 동작한다면 적절한 포인터를 넘기고 실패하면 (void*) -1을 반환하게 됩니다. 여기서 왜 void*를 반환하는지 아시겠죠? 공유 메모리에 들어가있는 데이터가 정수형인지, 부동 소수 형태인지, 또는 구조체인지 모르니까 무엇이든 받을 수 있는 void*으로 넘겨줄테니 알아서 반환해서 써라 이 이야기입니다.

 

3) shmdt : 공유메모리를 이 프로세스와 떼어냅니다. 이는 공유메모리를 제거하는 것이 아님에 주의하세요.

int shmdt(const void *shmaddr);

shmaddr : shmat에서 전달받은 그 포인터를 전달하면 됩니다.

성공 시 0, 실패시 -1을 반환합니다.

 

4) shmctl : 공유메모리를 제어하기 위해 사용합니다. 예를 들면 공유메모리의 정보를 얻거나 어떤 값을 쓰거나 공유메모리를 삭제하는 등의 조작이 있습니다.

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

shmid : 말 안해도 아시겠지만 공유메모리 id입니다.

cmd : 제어할 일종의 command입니다. 정수형을 갖으며 여러 command가 존재하는데 우리는 IPC_RMID라는 cmd를 예제를 통해 사용하도록 하겠습니다.

buf : shmid_ds라는 구조체로 정의되어 있네요. 어떤 구조체인지 봅시다.

struct shmid_ds {
       struct  ipc_perm shm_perm;    /* Ownership and permissions */
       size_t          shm_segsz;   /* Size of segment (bytes) */
       time_t          shm_atime;   /* Last attach time */
       time_t          shm_dtime;   /* Last detach time */
       time_t          shm_ctime;   /* Last change time */
       pid_t           shm_cpid;    /* PID of creator */
       pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
       shmatt_t        shm_nattch;  /* No. of current attaches */
       ...
};

 

복잡하게 보이지만 사실 옆의 주석만 좀 읽어보면 추측은 할 수 있겠네요. 

shm_perm : 공유메모리의 접근제어가 저장되어있습니다.

shm_segsz : 공유메모리의 size를 의미합니다.

shm_atime : attach된 시간 정보를 기록하네요.

shm_dtime : 반대로 detach된 시간을 기록합니다.

shm_ctime : 공유메모리가 변경될때의 시간을 기록합니다.

shm_cpid : 이 공유메모리를 최초로 만들어낸 process id입니다.

shm_lpid : 최근 shmat, 또는 shmdt를 수행한 process id입니다. 

 

첫번째 필드인 ipc_perm이라는 구조체도 한번 볼까요?

struct ipc_perm {
       key_t          __key;    /* Key supplied to shmget(2) */
       uid_t          uid;      /* Effective UID of owner */
       gid_t          gid;      /* Effective GID of owner */
       uid_t          cuid;     /* Effective UID of creator */
       gid_t          cgid;     /* Effective GID of creator */
       unsigned short mode;     /* Permissions + SHM_DEST and
                                           SHM_LOCKED flags */
       unsigned short __seq;    /* Sequence number */
};

 

아실테지만 uid, gid 등의 permission 정보가 있습니다. 

 

 

 

 

예제 

이제 공유메모리를 사용한 아주 간단한 예를 보도록 하겠습니다. 너무나 간단합니다.

#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main(){
        int shmid;
        int *num;
        key_t key=987654;
        void *memory_segment=NULL;

        if((shmid=shmget(key,sizeof(int),IPC_CREAT|0666))==-1){
                printf("shmget failed\n");
                exit(0);
        }


        if((memory_segment=shmat(shmid,NULL,0))==(void*)-1){
                printf("shmat failed\n");
                exit(0);
        }

        num=(int*)memory_segment;
        (*num)++;
        printf("shared memory value :%d\n",(*num));
        return 0;
}

 

공유메모리 id를 얻어와서 공유메모리의 값을 하나씩 증가시키고 있는 예제입니다. 보시다시피 공유메모리의 포인터는 void*를 반환하지만 우리는 int*로 연산할 것이기에 int*형변환을 하고 값을 증가시켰지요.

사용되는 key값은 아무 정수나 넣어주시면 됩니다. 

 

자, 컴파일하고 a.out을 연타해봅시다.

# gcc shm.c
# ./a.out
shared memory value :1
# ./a.out
shared memory value :2
# ./a.out
shared memory value :3
# ./a.out
shared memory value :4
# ./a.out

 

본래 프로세스가 끝나면 메모리도 정리가 되서 계속 1이라는 값이 출력이 되어야하는데 메모리가 아직 살아있고 그 메모리의 값을 참조하여 증가시키니 프로그램을 계속 실행시키면 이렇게 증가된 값을 보여주게 됩니다.

공유메모리가 아직 살아있다는 것을 어떻게 알까요?

 

ipcs -m 명령으로 볼 수 있습니다.

# ipcs -m

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0x00000000 4          root       600        524288     2          dest
0x000181cd 5          root       660        4          0
0x000181cb 6          root       666        4          0
0x000f1206 14         root       666        4          0

 

key값을 잘 보시기 바랍니다. 0x000f1206은 10진수로 987654, 즉 우리가 위 코드에 설정한 key값입니다.

 

저희는 프로그램이 종료되면 공유메모리도 종료시키고 싶습니다. 가령 아주 위험하게 이 공유메모리에 민감한 정보라도 있다면 큰일이잖아요? www.poxxhub.com/post_id=xxxxx

 

 

 

다음과 같이 코드를 고쳐봅시다. 

#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main(){
        int shmid;
        int *num;
        key_t key=987654;
        void *memory_segment=NULL;

        if((shmid=shmget(key,sizeof(int),IPC_CREAT|0666))==-1){
                printf("shmget failed\n");
                exit(0);
        }


        if((memory_segment=shmat(shmid,NULL,0))==(void*)-1){
                printf("shmat failed\n");
                exit(0);
        }

        num=(int*)memory_segment;
        (*num)++;
        printf("shared memory value :%d\n",(*num));
        if(shmctl(shmid,IPC_RMID,NULL)==-1){
                printf("shmctl failed\n");
        }

        return 0;
}

 

위 코드에 추가한것은 shmctl을 추가한것 밖에 없습니다. cmd로 IPC_RMID를 전달했네요. 그렇다면 이제 이 shmid는 사용할 수 없게 됩니다.

이제 컴파일하고 실행해봅시다.

# gcc shm.c
# ./a.out
shared memory value :5
# ./a.out
shared memory value :1
# ./a.out
shared memory value :1
# ./a.out
shared memory value :1
# ./a.out
shared memory value :1
# ./a.out
shared memory value :1

 

다시 ipcs -m 명령으로 공유메모리가 남아있는지 확인해보면 사라진것을 볼 수 있습니다.

# ipcs -m

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0x00000000 4          root       600        524288     2          dest
0x000181cd 5          root       660        4          0
0x000181cb 6          root       666        4          0

 

여기까지 공유메모리에 대해서 살펴보았는데요. IPC기법중에서도 그렇게 어렵지 않는 공유메모리를 사용할때 주의해야할 한 가지는 데이터가 오염되지 않게 처리하는 방법입니다. semaphore이든 mutex이든 메모리를 동시에 접근할때 처리를 해주어야하며 이 포스팅에서는 그러한 처리는 하지 않았습니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

파이프와 더불어 더 많은 정보를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

파이프(Pipe)

파이프(Pipe)란 프로세스간 통신을 할때 사용하는 커뮤니케이션의 한 방법입니다. 가장 오래된 UNIX 시스템의 IPC로 모든 유닉스 시스템이 제공합니다. 하지만 두가지 정도의 한계점이 있습니다.

 

첫번째 한계점으로 파이프는 기본적으로 반이중 방식입니다. 물론 전이중 방식을 지원하는 시스템이 있긴 하나, 최대의 이식성을 위해서는 파이프는 반이중 방식이라는 생각을 해야합니다. 이것은 FIFO라는 명명된 파이프로 극복할 수 있습니다.

두번째 한계점으로는 부모, 자식 관계에서의 프로세스들만 사용할 수 있습니다. 부모프로세스가 파이프를 생성하고, 이후 자식 프로세스와 부모프로세스가 파이프를 이용하여 통신합니다.

 

이러한 한계점이 잇긴 하지만 여전히 쓸모있는 IPC기법입니다.

 

파이프는 unistd.h 헤더파일이 존재합니다.

 

#include <unistd.h>
int pipe(int fd[2]);

pipe함수가 성공적으로 호출되었다면 0, 실패했을 경우 -1을 반환합니다.

인자 fd는 2개의 원소가 있는 배열이라는 점을 주목합시다. 2개의 원소를 쓰는 이유가 있습니다. 아래의 그림을 보면서 이해합시다.

 

 

파이프는 커널영역에 생성되어 파이프를 생성한 프로세스는 파일 디스크립터만 갖고 있게 됩니다. 여기서 파일디스크립터 fd[1]은 쓰기용 파이프, fd[0]은 읽기용 파이프입니다. 그러니 우리가 만약 데이터를 fd[1]에 쓰게 되면 fd[0]으로 그 데이터를 읽을 수 있는 것입니다.

 

그렇다면 자식 프로세스를 하나 더 두어서 자식과 부모가 통신할 수 있게 하려면 어떻게 해야할까요? 우선 자식 프로세스를 fork하면 파일 디스크립터는 부모의 파일디스크립터를 자식이 그대로 사용할 수 있는 것을 활용합니다. (파일디스크립터가 그대로 자식프로세스에 복제됩니다.)

부모프로세스는 파이프에 데이터를 쓰는 프로세스, 자식 프로세스는 그 파이프에서 데이터를 읽는 프로세스로 설계합시다.

 

 

우선 부모 프로세스에서 파이프를 생성하면 파이프에 데이터를 쓸것이기 때문에 읽기 파이프는 닫습니다. fd[0]이죠? 그런 후 fd[1]에 데이터를 씁니다.

자식 프로세스는 쓰기 파이프는 쓰지 않으므로 fd[1]을 닫고, 읽기 파이프로 데이터를 읽습니다.

 

다음은 그런 기능을 하는 코드입니다.

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define MAX_BUF 1024
#define READ 0
#define WRITE 1
int main(){
        int fd[2];
        pid_t pid;
        char buf[MAX_BUF];

        if(pipe(fd) < 0){
                printf("pipe error\n");
                exit(1);
        }
        if((pid=fork())<0){
                printf("fork error\n");
                exit(1);
        }

        printf("\n");
        if(pid>0){ //parent process
                close(fd[READ]);
                strcpy(buf,"message from parent\n");
                write(fd[WRITE],buf,strlen(buf));
        }else{  //child process
                close(fd[WRITE]);
                read(fd[READ],buf,MAX_BUF);
                printf("child got message : %s\n",buf);
        }
        exit(0);
}

 

결과는 아래와 같습니다.

 

child got message : message from parent

 

자식 프로세스에서 부모 프로세스가 pipe에 쓴 데이터를 읽었습니다. 

또는 자식프로세스가 데이터를 쓰고, 부모프로세스가 데이터를 읽는 설계도 가능하겠죠.

 

 

그렇다면 부모 프로세스와 자식 프로세스가 읽기, 쓰기가 가능하게 구현하려면 어떻게 해야할까요? 파이프를 한개만 사용한다고 해봅시다.

 

그리고 이런 상황을 가정해보지요.

1. 먼저 부모프로세스가 파이프에 fd[1]로 데이터를 보냅니다. 

2. 그 이후 자식 프로세스가 부모 프로세스가 쓴 데이터를 fd[0]으로 읽습니다.

3. 자식 프로세스는 바로 fd[1]로 파이프에 응답값을 보냅니다.

4. 부모 프로세스는 fd[0]으로 자식 프로세스가 보낸 응답값을 읽습니다.

 

결론을 말씀드리면 항상 위의 상황은 발생하지 않습니다. 그 이유는 누가 먼저 파이프를 읽느냐에 따라서 결과가 달라지는데, 만일 부모프로세스가 파이프에 쓰고, 자식 프로세스가 그 데이터를 읽기도 전에 부모프로세스가 먼저 데이터를 읽는다면 파이프에 데이터는 없겠죠. 허나 자식 프로세스는 없는 데이터를 계속 읽기만 기다리고 있기 때문에 프로그램이 망하게 되는 겁니다.

 

이때는 파이프를 2개 사용해야합니다.

fdA와 fdB 2개 사용합니다. 부모프로세스는 자식에게 쓰기용으로 fdA[1], 자식프로세스로부터 읽기용으로 fdB[0]만 있으면 됩니다. 필요없는 fdA[0], fdB[1]은 닫아줍니다.

그리고 자식프로세스는 부모프로세스로부터 읽기용으로 fdA[0], 쓰기용으로 fdB[1]만 있으면 되지요. 역시 필요없는 fdA[1], fdB[0]은 닫아줍니다.

이제 이런 개념으로 코드를 구현합시다.

 

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define MAX_BUF 1024
#define READ 0
#define WRITE 1
int main(){
        int fdA[2],fdB[2];
        pid_t pid;
        char buf[MAX_BUF];
        int count=0;

        if(pipe(fdA) < 0){
                printf("pipe error\n");
                exit(1);
        }

        if(pipe(fdB) < 0){
                printf("pipe error\n");
                exit(1);
        }

        if((pid=fork())<0){
                printf("fork error\n");
                exit(1);
        }

        printf("\n");
        if(pid>0){ //parent process
                close(fdA[READ]);
                close(fdB[WRITE]);
                while(1){
                        sprintf(buf,"parent %d",count++);
                        write(fdA[WRITE],buf,MAX_BUF);
                        memset(buf,0,sizeof(buf));
                        read(fdB[READ],buf,MAX_BUF);
                        printf("parent got message : %s\n",buf);
                        sleep(1);
                }
        }else{  //child process
                close(fdA[WRITE]);
                close(fdB[READ]);
                count=100000;
                while(1){
                        sprintf(buf,"child %d",count++);
                        write(fdB[WRITE],buf,MAX_BUF);
                        memset(buf,0,sizeof(buf));
                        read(fdA[READ],buf,MAX_BUF);
                        printf("\tchild got message : %s\n",buf);
                        sleep(1);
                }
        }
        exit(0);
}

 

 

부모 프로세스는 0부터 1초마다 증가한 값을 파이프에 쓰고, 자식 프로세스로부터 파이프로 읽습니다. 자식 프로세스는 100000부터 증가한 값을 1초마다 쓰고, 읽습니다. 그 결과는 아래와 같습니다.

        child got message : parent 0
parent got message : child 100000
        child got message : parent 1
parent got message : child 100001
        child got message : parent 2
parent got message : child 100002
        child got message : parent 3
parent got message : child 100003
        child got message : parent 4
parent got message : child 100004
        child got message : parent 5
parent got message : child 100005

       

부모 자식 관계의 프로세스가 아닌 별개의 프로세스가 통신할때는 아까 위에서 말씀드린 것 처럼 FIFO를 사용해야합니다. 

 

파이프를 이용한 IPC구현, 이제 어렵지 않겠죠?

 

반응형
블로그 이미지

REAKWON

와나진짜

,