본문 바로가기

프로그래밍(TA, AA)/C C++

[통신프로그래밍] IPC(메시지큐, 공유 메모리, 세마포어)

유닉스는 처음 개발된 이래 다양한 형태로 발전했는데, 크게 BSD 계열과 시스템 V 계열로 구분할 수 있습니다. 이중 시스템 V 계열 유닉스에서 개발해 제공하는 프로세스 간 통신 방법이 메시지 큐, 공유 메모리, 세마포어입니다. 흔히 이 세가지를 묶어서 시스템 V IPC라고 합니다. 시스템 V IPC는 SVR2에서 처음 개발되었고, SVR4에서도 제공하고 있으며 현재는 대부분의 유닉스 시스템에서 제공하고 있습니다.


시스템 V IPC를 사용하려면 IPC의 객체를 생성해야 합니다. 이를 위해 공통적으로 사용하는 기본 요소가 키와 식별자입니다. 또한 현재 사용 중인 각 IPC의 상태를 확인하고, 사용을 마친 객체는 삭제할 수 있도록 관리 명령을 제공합니다. 키의 생성 및 IPC 관리 명ㄹ영르 아래에 요약해 두었습니다.


 기능

 함수원형 및 명령

 상수

 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]



메시지 큐는 파이프와 유사합니다. 단, 파이프는 스트림 기반으로 동작하고, 메시지 큐는 메시지(또는 패킷) 단위로 동작합니다. 메시지 큐는 우편함과 비슷하다고 생각하면 됩니다. 우편함처럼 메시지 큐를 만든 후 이를 통해 메시지를 주고받습니다. 메시지 큐에서 제공하는 함수는 아래와 같습니다.


 기능

 함수원형

 메시지 큐 생성

 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);



공유 메모리는 한 프로세스의 일부분을 다른 프로세스와 공유하는 것을 의미합니다. 즉, 메모리의 일부 공간을 두 독립적인 프로세스에서 공유하고, 해당 메모리를 통해 데이터를 주고받을 수 있습니다. 공유 메모리 관련 함수는 아래와 같습니다.


 기능

 함수원형

 공유 메모리 생성

 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);




01. 시스템 V IPC의 공통 요소

시스템 V IPC에서 공통적으로 사용하는 기본 요소는 키와 식별자입니다. 메시지 큐, 공유 메모리, 세마포어를 사용하려면 식별자가 있어야 합니다. 또한 시스템V IPC에서는 현재 사용 중인 각 통신 방법의 상태를 확인하고, 불필요한 것은 정리할 수 있는 관리 명령을 제공합니다. 이 절에서는 시스템 V IPC의 기본 요소인 키와 식별자의 용도를 알아보고, 관련 함수와 명령을 살펴보겠습니다.



키와 식별자


시스템V IPC에서 사용하는 키는 key_t 형으로 <sys/types.h>에 선언되어 있으며, 32비트 정수형입니다. IPC를 사용하기 위해 새로운 IPC 객체를 생성할 때 키를 지정합니다. 키가 다르면 서로 다른 객체가 생성됩니다. 각 IPC 방법의 객체 생성 함수에서 키를 받아 새로운 IPC 객체를 생성하고, 이를 식별하는 식별자를 리턴합니다. 이 식별자를 사용해 통신할 수 있습니다.


키의 생성

시스템V IPC에서 사용하는 키는 다음과 같이 생성할 수 있습니다.


  • 키로 IPC_PRIVATE를 지정한다. 식별자를 알아야 통신할 수 있으므로 IPC_PRIVATE를 키로 지정해 생성된 식별자를 서버와 클라이언트 모두 알 수 있게 해야 한다. fork 함수로 생성된 부모-자식 프로세스 간 통신에서 유용하게 사용할 수 있다. 
  • ftok 함수로 키를 생성한다. ftok 함수는 경로명과 숫자값을 받아서 키를 생성한다. 따라서 서버와 클라이언트가 같은 경로명과 숫자값을 지정하면 공통 식별자를 생성할 수 있다.


같은 키로 생성된 식별자는 통신에 사용할 수 있습니다. 따라서 미리 정해진 키를 서버와 클라이언트 프로세스가 공유할 수 있게 해야합니다. 헤더 파일이나 환경 설정 파일에 키를 저장해 공유할 수 있습니다. 단, 이 키를 제3의 프로세스가 먼저 사용하고 있으면 안됩니다.



키 생성하기 : ftok(3)

#include <sys/ipc.h>

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

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


키를 구성하는 32비트 중 처음 12비트에는 stat 구조체의 st_dev 값, 다음 12비트에는 st_ino 값이 저장됩니다. 마지막 8비트에 ftok 함수의 두번째 인자로 지정한 정수값이 저장됩니다. 마지막 8비트에 ftok 함수의 두 번째 인자로 지정한 정수값이 저장됩니다. 따라서 같은 경로명과 같은 정수값을 지정하면 같은 식별자가 리턴됩니다. 경로명은 같지만 정수값이 다르면 다른 식별자가 리턴됩니다. 정수값이 같더라도 경로명이 다르면 다른 식별자가 리턴됩니다. 이 구조는 아래 표를 보면 알 수 있습니다.


st_dev(12비트)

st_ino(12비트) 

id(8비트) 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 



경로명으로 지정한 파일에 대해 stat 함수를 실행할 수 있어야 합니다. 따라서 키가 참조하고 있는 경로의 파일이 삭제되면 ftok 함수는 오류를 발생시킵니다. 삭제된 파일을 다시 생성하고 같은 숫자를 id로 지정한 후 ftok 함수로 키를 생성해도 다른 키가 생성됩니다. 위 그림에 나타낸 키의 구성요소 중 inode(st_ino) 값이 달라지기 때문입니다.


키의 마지막 8비트에 정수값이 저장되므로, 1~255 사이의 값을 지정해야 합니다. 이 값을 0으로 지정하면 ftok 함수가 어떻게 동작할지 정의되어 있지 않으므로 사용하지 않는 것이 좋습니다. ftok 함수는 다음과 같은 형태로 사용합니다. 다음 예는 경로명으로 /export/home/keyfile을 지정하고 정수값으로 1을 지정한 것입니다.

key_t key;
key = ftok("/export/home/keyfile", 1);



IPC 공통 구조체


시슷템 V IPC를 사용하기 위해 해당 IPC의 객체를 생성하면 IPC 공통 구조체가 정의됩니다. 시스템 V 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입3니다. ipcs 명령을 실행하는 동안에도 IPC의 상태가 변경될 수 있습니다. ipcs 명령은 검색하는 순간의 정확성만 보장합니다. 상태가 변경된 정보를 보려면 ipcs 명령을 다시 수행해야 합니다.


ipcs 명령의 기본 형식

ipcs 명령의 기본 형식은 다음과 같으며, 사용할 수 있는 옵션의 종류와 의미를 살펴보겠습니다.


ipcs [-aAbciJmopqstZ] [-D mtype]


  • -m: 공유 메모리에 관한 정보만 검색한다.
  • -q: 메시지 큐에 관한 정보만 검색한다.
  • -s: 세마포어에 관한 정보만 검색한다.(이 세 옵션이 없으면 세 가지 모두에 관한 정보가 검색된다.)
  • -a: -b, -c, -o, -p, -t 옵션으로 검색하는 항목을 모두 출력한다.
  • -A: 전체 항목(-b, -c, -i, -J, -o, -p, -t)을 모두 검색한다.
  • -b: 각 방법의 최댓값(메시지 큐에서 사용 가능한 최대 메시지 크기(바이트 수), 공유 메모리의 세그먼트 크기, 세마포의 수)을 검색한다.
  • -c: IPC 객체를 생성한 사용자의 로그인명과 그룹명을 검색한다.
  • -D mtype: 메시지 큐에서 mtype으로 지정한 메시지만 검색한다. mtype 값이 0일 경우 모든 메시지가 출력된다. mtype 값이 음수면 이 값의 절댓값보다 같거나 작은 모든 메시지가 출력된다.
  • -i: 공유 메모리 세그먼트에 연결된 ISM의 개수가 출력된다.
  • -J: IPC 객체 생성자의 프로젝트명이 출력된다.
  • -o: 현재 사용되고 있는 정도로 출력한다. 메시지 큐에 있는 메시지의 수와 전체 메시지의 크기 정보, 공유 메모리 세그먼트에 연결된 프로세스의 개수 정보를 검색한다.
  • -p: PID 정보를 출력한다. 메시지 큐에서 메시지를 보내거나 검색한 마지막 프로세스의 ID, 공유 메모리에 연결하거나 해제한 마지막 프로세스의 ID 정보를 검색한다.
  • -t: 시간 정보를 출력하다. 전체 IPC 객체에서 접근 권한을 변경한 마지막 제어 동작 수행 시각, 메시지 큐에서 마지막으로 메시지를 보낸 시각과 마지막으로 메시지를 받은 시각, 공유 메모리에서 마지막으로 연결과 해제 동작을 한 시각, 세마포어에서 마지막 세마포어 동작을 수행한 시각 정보가 검색된다.


ipcs 명령의 출력 형식

ipcs 명령의 다양한 옵션을 적절히 조합해 원하는 정보를 검색할 수 있습니다. 아무 옵션 없이 ipcs 명령을 사용하면 현재 동작 중인 메시지 큐와 공유 메모리, 세마포어에 대한 간단한 정보를 검색합니다. 다음 예는 현재 동작 중인 IPC 객체가 하나도 없음을 의미합니다.


[root@dev-web ch10]# ipcs


------ Message Queues --------

key        msqid      owner      perms      used-bytes   messages


------ Shared Memory Segments --------

key        shmid      owner      perms      bytes      nattch     status


------ Semaphore Arrays --------

key        semid      owner      perms      nsems


ipcs 명령에서 출력하는 항목은 다음과 같습니다.


 플래그

 모드

 설명

 T

 전체

 IPC 객체의 종류를 표시한다.

 - q: 메시지 큐, m: 공유 메모리 세그먼트, s: 세마포어

 ID

 전체

 IPC 객체의 식별자를 표시한다.

 KEY

 전체

 IPC 객체를 생성할 때 인자로 지정하는 키다.

 MODE

 전체

 접근 권한을 나타내는 11개 문자다. 처음 두 문자는 특별한 동작을 표시하는 플래그다.

 - R: 프로세스가 msgrcv를 기다림.

 - S: 프로세스가 msgsnd를 기다림.

 - D: 관련된 공유 메모리가 삭제됨.

 - C: 관련된 공유 메모리 세그먼트가 초기화됨.

 - -: 특별 플래그가 설정되어 있지 않음.

   나머지 9개 문자는 3개씩 묶어서 사용자, 그룹, 기타 사용자의 접근권한을 표시한다.

 - r: 읽기 권한 부여

 - w: 쓰기 권한 부여

 - a: 변경 권한 부여

 - -: 해당 권한 부여

 OWNER

 전체

 IPC 객체 소유자의 로그인명

 GROUP

 전체

 IPC 객체를 소유하고 있는 그룹명

 CREATOR

 a, A, c

 IPC 객체를 생성한 사용자의 로그인명

 CGROUP

 a, A, c

 IPC 객체를 생성한 그룹명

 CBYTES

 a, A, o

 현재 메시지 큐에 있는 메시지의 바이트 수

 QNUM

 a, A, o

 현재 메시지 큐에 있는 메시지의 개수

 QBYTES

 a, A, b

 메시지 큐에 허용된 최대 바이트 수

 LSPID

 a, A, p

 메시지 큐에 메시지를 보낸 마지막 프로세스의 ID

 LRPID

 a, A, p

 메시지 큐의 메시지를 마지막으로 읽은 프로세스의 ID

 STIME

 a, A, t

 마지막 메시지가 관련 큐로 보내진 시각

 RTIME

 a, A, t

 마지막 메시지를 읽은 시각

 CTIME

 a, A, t

 관련 항목이 생성되거나 수정된 시각

 ISMATTACH

 a, i

 관련 공유 메모리에 ISM 연결을 한 개수

 NATTACH

 a, A, o

 관련 공유 메모리 세그먼트에 연결된 프로세스의 수

 SEGSZ

 a, A, b

 관련 공유 메모리 세그먼트의 크기

 CPID

 a, A, p

 공유 메모리를 생성한 생성자의 프로세스 ID

 LPID

 a, A, p

 공유 메모리 세그먼트를 연결하거나 해제한 마지막 프로세스의 ID

 ATIME

 a, A, t

 관련 공유 메모리에 마지막으로 연결한 시각

 DTIME

 a, A, t

 관련 공유 메모리에 대한 연결을 마지막으로 해제한 시각

 NSEMS

 a, A, b

 세마포어 항목의 집합에 현재 남아 있는 세마포어 수

 OTIME

 a, A, t

 세마포어 항목의 집합에서 마지막 세마포어 동작이 끝난 시각

 PROJECT

 J, A

 IPC 객체를 생성한 생성자의 프로젝트 명




시스템 V IPC 정보 삭제


시스템 V IPC의 정보를 검색하고 이중 불필요한 IPC 객체를 삭제하려면 ipcrm 명령을 사용합니다. ipcrm 명령의 기본 형식은 다음과 같습니다.


ipcrm [-m shmid] [-q msquid] [-s semid] [-M shmkey] [-Q msgkey] [-S semky]


  • -m shmid: shmid로 지정한 공유 메모리를 삭제한다. 공유 메모리에 대한 마지막 해제 동작 이후에 관련된 메모리 세그먼트와 데이터 구조체가 제거된다.
  • -q msqid: msqid로 지정한 메시지 큐와 데이터 구조체를 삭제한다.
  • -s semid: semid로 지정한 세마포어와 데이터 구조체를 삭제한다.
  • -M shmkey: shmkey로 생성한 공유 메모리를 삭제한다. 공유 메모리에 대한 마지막 해제 동작 이후에 관련된 메모리 세그먼트와 데이터 구조체가 제거된다.
  • -Q msgkey: msgkey로 생성한 메시지 큐와 데이터 구조체를 삭제한다.
  • -S semkey: semkey로 생성한 세마포어와 데이터 구조체를 삭제한다.



02. 메시지 큐

메시지 큐(message queue)는 파이프(pipe)와 유사합니다. 단, 파이프는 스트림 기반으로 동작하고, 메시지 큐는 메시지(또는 패킷) 단위로 동작합니다. 각 메시지의 최대 크기는 제한되어 있습니다. 메시지 큐는 우편함과 비슷하다고 생각하면 됩니다. 우편함처럼 메시지 큐를 만든 후 이를 통해 메시지를 주고 받습니다. 각 메시지에는 메시지 유형(message type)이 있으므로, 수신 프로세스는 어떤 유형의 메시지를 받을 것인지 선택할 수 있습니다. 이 절에서는 메시지 큐 관련 함수를 살펴보겠습니다.



메시지 큐 관련 함수


 메시지 큐와 관련해 메시지 큐 생성, 메시지 전송, 메시지 수신, 메시지 큐 제어 함수가 제공됩니다. 메시 큐 제어 함수는 메시지 큐를 제거하거나 상태 정보를 설정하고 읽어오는 등 메시지 큐에 대한 제어 기능을 수행합니다.


메시지 큐 생성: msgget(2)

메시지 큐를 사용하려면 메시지 큐 식별자를 생성해야 합니다. msgget 함수를 사용하면 메시지 큐 식별자를 생성할 수 있습니다.

#include <sys/msg.h>

int msgget(key_t key, int msgflg);

msgget 함수는 인자로 키와 플래그를 받아 메시지 큐 식별자를 리턴합니다. 첫번째 인자인 key에는 IPC_PRIVATE 나 ftok 함수로 생성한 키를 지정합니다. 두번째 인자인 msgflg에는 플래그와 접근 권한을 지정합니다. 사용할 수 있는 플래그는 다음과 같으며, <sys/ipc.h>에 정의되어 있습니다.


  • IPC_CREAT(0001000) : 새로운 키면 식별자를 새로 생성한다.
  • IPC_EXCL(0002000) : 이미 존재하는 키면 오류가 발생한다.


메시지 큐 식별자와 관련된 메시지 큐와 IPC 구조체가 새로 생성되는 경우는 다음 두가지입니다.


  • key가 IPC_PRIVATE이다.
  • key가 0이 아니며 다른 식별자와 관련되어 있지 않고, 플래그(msgflg)에 IPC_CREAT가 설정되어 있다.


이 두 가지 경우가 아니면 msgget 함수는 기존 메시지 큐의 식별자를 리턴합니다.


메시지 큐에 관한 정보를 담고 있는 구조체는 msqid_ds로, <sys/msg.h>에 다음과 같이 정의되어 있습니다.

struct msqid_ds {
    struct ipc_perm     msg_perm;     // IPC 공통 구조체(ipc_perm)을 의미한다.
    struct msg          *msg_first;   // 메시지 큐에 있는 첫번째 메시지에 대한 포인터다.
    struct msg          *msg_last;    // 메시지 큐에 있는 마지막 메시지에 대한 포인터다.
    msglen_t            msg_cbytes;   // 현재 메시지 큐에 있는 메시지의 총 바이트 수다.
    msgqnum_t           msg_qnum;     // 메시지 큐에 있는 메시지의 개수다
    msglen_t            msg_qbytes;   // 메시지 큐의 최대 크기(바이트 수)다.
    pid_t               msg_lspid;    // 마지막으로 메시지를 보낸 프로세스의 ID다.
    pid_t               msg_lrpid;    // 마지막으로 메시지를 읽은 프로세스의 ID다.
    time_t              msg_stime;    // 마지막 으로 메시지를 보낸 시각이다.
    int32_t             msg_pad1;     // 시간 정보를 확장하기 위한 예비 공간이다.
    time_t              msg_rtime;    // 마지막으로 메시지를 읽은 시각이다.
    int32_t             msg_pad2;     // 시간 정보를 확장하기 위한 예비 공간이다.
    time_t              msg_ctime;    // 마지막으로 메시ㅣ 큐의 권한을 변경한 시각이다.
    int32_t             msg_pad3;     // 시간 정보를 확장하기 위한 예비 공간이다.
    short               msg_cv;
    short               msg_qnum_cv;
    long                msg_pad4[3];
}


식별자가 리턴할 때 메시지 큐의 구조체는 다음과 같이 설정됩니다.


  • msg_perm.cuid, msg_perm.uid: 함수를 호출한 프로세스의 유효 사용자 ID로 설정된다.
  • msg_perm.cgid, msg_perm.gid: 함수를 호출한 프로세스의 유효 그룹 ID로 설정된다.
  • msg_perm.mode의 하위 9비트: msgflg 값의 하위 9비트로 설정된다.
  • msg_qnum, msg_lgpid, msg_lrpid, msg_stime, msg_rtime: 0으로 설정된다.
  • msg_ctime: 현재 시각으로 설정된다.
  • msg_qbytes: 시스템의 제한값으로 설정된다.


msgget 함수는 수행을 성공하면 메시지 큐 식별자를 음수가 아닌 정수로 리턴하고, 실패하면 -1을 리턴합니다. msgget 함수로 메시지 큐 식별자를 생성하는 예는 다음과 같습니다.

key_t key;
int id;

key = ftok("keyfile", 1);
id = msgget(key, IPC_CREAT|0640);

위의 예는 메시지 큐의 식별자를 새로 생성하고 있으며, 접근 권한은 0640(rw-r-----)입니다.



메시지 전송: msgsnd(2)

메시지 큐로 메시지를 보낼 때는 msgsnd 함수를 사용합니다.

#include <sys/msg.h>

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
  • msqid: msgget 함수로 생성한 메시지 큐 식별자
  • msgp: 메시지를 담고 있는 메시지 버퍼의 주소
  • msgsz: 메시지의 크기(0~시스템이 정한 최댓값)
  • msgflg: 블록 모드(0) / 비블록 모드(IPC_NOWAIT)


msgsnd 함수는 msgget 함수가 리턴한 메시지 큐(msqid)를 통해 크기가 msgsz인 메시지를 메시지 버퍼(msgp)에 담아 전송합니다. msgflg에는 메시지 큐가 가득 찼을 때의 동작을 지정합니다.


메시지를 담고 있는 메시지 버퍼는 msgbuf 구조체를 사용합니다. msgbuf 구조체는 <sys/msg.h> 파일에 다음과 같이 정의되어 있습니다.

struct msgbuf {
    long mtype;
    char mtext[1];
}
  • mtype: 메시지 유형으로, 양수를 지정한다.
  • mtext: msgsnd 함수의 msgsz로 지정한 크기의 버퍼로, 메시지 내용이 저장된다.


msgbuf 구조체는 메시지 큐를 생성할 때 앞의 형식대로 사용자가 정의해 사용합니다. 이는 아래 예제를 통해 확인해보겠습니다.


세 번째 인자인 msgsz에는 전송하는 메시지의 크기를 지엏바니다. 마지막 인자인 msgflg에는 0이나 IPC_NOWAIT를 지정합니다. 메시지 큐가 가득 찬 경우, msgflg가 0이면 기다리고, IPC_NOWAIT이면 기다리지 않고 바로 오류를 리턴합니다. msgsnd 함수의 수행이 성공하면 msqid_ds 구조체의 항목에서 msg_qnum 값이 1 증가하고, msg_lspid는 msgsnd 함수를 호출한 프로세스의 ID로 설정됩니다. msg_stime은 현재 시각으로 설정됩니다. msgsnd 함수는 수행을 실패하면 -1을 리턴하고 메시지는 전송하지 않습니다.

#include <sys/msg.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

// 메시지 버퍼로 사용할 구조체를 문법에 따라 정의한다.
// 메시지를 저장할 배열의 크기는 송수신할 메시지의 크기에 따라 적절하게 조절하면 된다.
typedef struct mymsgbuf {
    long mtype;
    char mtext[80];
}MyMsgBuf;

int main(void) {
    key_t key;
    int msgid;
    MyMsgBuf mesg;

    key = ftok("keyfile", 1);               // 키를 정의하고
    msgid = msgget(key, IPC_CREAT|0644);    // 메시지 식별자를 생성한다.
    if(msgid == -1) {
        perror("msgget");
        exit(1);
    }

    // 전송할 메시지 버퍼를 설정한다.
    mesg.mtype = 1; // 메시지 유형을 1로 정의
    strcpy(mesg.mtext, "Message Q Test\n"); // 메시지 버퍼의 배열에 문자열을 복사.

    if(msgsnd(msgid, (void *)&mesg, 80, IPC_NOWAIT) == -1) { // msgsnd 함수를 사용해 메시지를 전송한다.
        perror("msgsnd");
        exit(1);
    }

    return 0;
}

[root@dev-web ch10]# ipcs -q


------ Message Queues --------

key        msqid      owner      perms      used-bytes   messages

0xffffffff 0          root       644        80           1


생성된 메시지 큐를 ipcs 명령으로 확인해보겠습니다. 실행 결과를 보면 생성된 메시지 큐의 식별자가 1임을 알 수 있습니다. 접근 권한을 0644로 지정했으므로 mode 값이 rw-r--r--로 지정되었습니다. QNUM이 1로 증가하고, 메시지의 바이트 수가 80바이트임을 알 수 있습니다.



메시지 수신: msgrcv(2)

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)


msqid가 가리키는 메시지 큐에서 msgtyp이 지정하는 메시지를 읽어 msgp가 가리키는 메시지 버퍼에 저장합니다. 메시지 버퍼의 크기는 msgsz에 지정하고, msgtflg는 메시지 큐가 비었을 때 어떻게 동작할 것인지 알려줍니다.


네번째 인자인 msgtyp에 지정할 수 있는 값은 다음과 같습니다.


  • 0: 메시지 큐의 다음 메시지를 읽어온다.
  • 양수: 메시지 큐에서 msgtyp으로 지정한 유형과 같은 메시지를 읽어온다.
  • 음수: 메시지의 유형이 msgtyp으로 지정한 값의 절댓값과 같거나 작은 메시지를 읽어온다.


다섯 번째 인자인 msgflg에는 msgsnd처럼 블록 모드/비블록 모드를 지정합니다. msgflg가 0이면 메시지 큐에 메시지가 올때까지 기다립니다. msgflg가 IPC_NOWAIT이면 메시지 큐가 비었을 때 기다리지 않고 즉시 오류를 리턴합니다. msgrcv 함수의 수행이 성공하면 msqid_ds 구조체 항목에서 msg_qnum 값이 1 감소하고, msg_lrpid는 msgrcv 함수를 호출한 프로세스의 ID로 설정됩니다. msg_rtime은 현재 시각으로 설정됩니다. msgrcv 함수는 수행을 성공하면 읽어온 메시지의 바이트 수를 리턴합니다. 실패하면 -1을 리턴하고 메시지를 읽어오지 않습니다.


다음은 보낸 메시지를 읽어서 출력한 예제입니다.

#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;
}

[root@dev-web ch10]# ./ex10_2.out

Received Msg = Message Q Test

, Len=80


[root@dev-web ch10]# ipcs -q


------ Message Queues --------

key        msqid      owner      perms      used-bytes   messages

0xffffffff 0          root       644        0            0


실행 결과를 보면 보낸 메시지를 제대로 읽어왔음을 알 수 있습니다. ipcs -qo 명령으로 메시지 큐의 상태를 보면 큐의 메시지 개수가 1에서 0으로 감소했음을 알 수 있습니다. 메시지 큐 내의 총 바이트 수도 0으로 감소했습니다.



메시지 제어: msgctl(2)

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로 지정한 메모리에 저장합니다.


msgctl 함수는 수행을 성공하면 0을, 실패하면 -1을 리턴합니다. 다음은 수행할 제어기능으로 IPC_RMID를 지정해 이전 생성한 메시지큐를 삭제하는 예제입니다.

#include <sys/msg.h>
#include <stdlib.h>
#include <stdio.h>

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;
}

[root@dev-web ch10]# ./ex10_3.out

Before IPC_RMID


------ Message Queues --------

key        msqid      owner      perms      used-bytes   messages

0xffffffff 0          root       644        0            0


After IPC_RMID


------ Message Queues --------

key        msqid      owner      perms      used-bytes   messages


실행 결과를 보면 msgctl 함수를 호출하기 전에는 메시지 큐가 존재했는데, 제거 명령어 실행후에는 메시지 큐가 제거되어 보이지 않습니다. 메시지 큐 사용을 마치면 이와 같이 msgctl 함수를 사용해 반드시 메시지 큐를 삭제해야 합니다.



03. 공유 메모리

공유 메모리는 같은 메모리 공간을 두 개 이상의 프로세스가 공유하는 것입니다. 같은 메모리 공간을 사용하므로 이를 통해 데이터를 주고받을 수 있습니다. 여러 프로세스가 메모리를 공유하고 있으므로 당연히 읽고 쓸 때 동기화가 필요합니다. 공유 메모리를 동기화하지 않을 경우 데이터가 손실 될 수 있습니다. 이 절에서는 공유 메모리를 생성하고 사용하는 데 필요한 함수를 살펴보겠습니다.



공유 메모리 관련 함수


공유 메모리와 관련해서는 공유 메모리를 생성하고 생성된 공유 메모리와 연결하며 사용을 마친 공유 메모리를 해제하는 함수와 공유 메모리를 제어하는 함수가 제공됩니다. 공유 메모리 제어 함수는 메시지 큐 제어 함수와 마찬가지로 공유 메모리 제거, 잠금 설정, 잠금 해제 등의 제어 기능을 수행합니다.


공유 메모리 생성: shmget(2)

공유 메모리를 사용하려면 공유 메모리 식별자를 생성해야 합니다. 공유 메모리 식별자는 shmget 함수를 사용해 생성합니다.

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

// 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입니다.


공유 메모리 식별자와 관련된 공유 메모리와 데이터 구조체가 새로 생성되는 경우 다음 두가지입니다.


  • key가 IPC_PRIVATE다.
  • key가 0이 아니며 다른 식별자와 관련되어 있지 않고, 플래그(msgflg)에 IPC_CREAT가 설정되어 있다.


이 두 가지 경우가 아니면 shmget 함수는 기존 공유 메모리의 식별자를 리턴합니다.


공유 메모리에 관한 정보를 담고 있는 구조체는 shmid_ds로, <sys/shm.h>에 다음과 같이 정의되어 있습니다.

struct shmid_ds {
    struct ipc_perm     shm_perm;     // IPC 공통 구조체(ipc_perm)를 의미
    size_t              shm_segsz;    // 공유 메모리 세그먼트의 크기를 바이트 단위로 나타낸다.
    struct anon_map     *shm_amp;     //
    pid_tt_t            shm_lpid;     // 마지막으로 shmop 동작을 한 프로세스의 ID
    pid_t               shm_cpid;     // 공유 메모리를 생성한 프로세스의 ID
    shmatt_t            shm_nattch;   // 공유 메모리를 연결하고 있는 프로세스의 개수
    ulong_t             shm_cnattch;  //
    time_t              shm_atime;    // 마지막으로 공유 메모리를 연결(shmat)한 시각
    int32_t             shm_pad1;     //
    time_t              shm_dtime;    // 마지막으로 공유 메모리의 연결을 해제(shmdt)한 시각
    int32_t             shm_pad2;     //
    time_t              shm_ctime;    // 마지막으로 공유 메모리의 접근 권한을 변경한 시각
    int32_t             shm_pad3;     //
    int32_t             shm_pad4[4];  //
};


새로운 공유 메모리 식별자를 리턴할 때 구조체는 다음과 같이 설정됩니다.


  • shm_perm.cuid, shm_perm.uid: 함수를 호출한 프로세스의 유효 사용자 ID로 설정된다.
  • shm_perm.cgid, shm_perm.gid: 함수를 호출한 프로세스의 유효 그룹 ID로 설정된다.
  • shm_perm.mode의 하위 9비트: msgflg 값의 하위 9비트로 설정된다.
  • shm_segsz: size 값으로 설정한다.
  • shm_lpid, shm_nattch, shm_atime, shm_dtime: 0으로 설정된다.
  • shm_ctime: 현재 시각으로 설정된다.


아래는 공유할 메모리의 크기가 1KB인 공유 메모리를 생성하는 예제입니다. 예제를 살펴본 다음 생성한 공유 메모리를 활용하는 함수를 살펴보겠습니다.

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

int main(void) {
    key_t key;
    int shmid;

    key = ftok("shmfile", 1);   // ftok 함수를 사용해 키를 생성한다.
    shmid = shmget(key, 1024, IPC_CREAT|0644);  // shmget 함수를 사용해 공유 메모리를 생성한다.
    if(shmid == -1) {
        perror("shmget");
        exit(1);
    }

    return 0;
}

[root@dev-web ch10]# ./ex10_4.out
[root@dev-web ch10]# ipcs -m

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0xffffffff 0          root       644        1024       0



공유 메모리 연결: shmat(2)

생성된 공유 메모리를 사용하려면 공유 메모리를 프로세스의 데이터 영역과 연결해야 합니다. 공유 메모리를 연결하려면 shmat 함수를 사용합니다.

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

void *shmat(int shmid, const void *shmaddr, int shmflg);
  • shmid: shmget 함수로 생성한 공유 메모리 식별자
  • shmaddr: 공유 메모리를 연결할 주소
  • shmflg: 공유 메모리에 대한 읽기/쓰기 권한

shmget 함수로 생성한 공유 메모리의 식별자를 shmid에 지정하고, shmaddr에 공유 메모리를 연결할 주소를 지정합니다. shmaddr에는 특별한 경우가 아니면 0을 지정합니다. 값이 0이면 시스템이 알아서 적절한 주소에 공유 메모리를 연결합니다. shmflg는 플래그로, 0이면 공유 메모리에 대해 읽기와 쓰기가 가능하고, SHM_RDONLY면 읽기만 가능합니다. shmat 함수는 수행을 성공하면 연결된 공유 메모리의 시작 주소를 리턴합니다.


공유 메모리 연결 해제: shmdt(2)

공유 메모리의 사용을 마치면 연결을 해제해야 합니다. 공유 메모리의 연결을 해제할 때는 shmdt 함수를 사용합니다.

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

int shmdt(char *shmaddr);

shmaddr에 공유 메모리의 시작 주소를 지정합니다. 이 시작 주소는 shmat 함수의 리턴값입니다. shmdt 함수의 사용 예도 이후 예제에서 살펴보겠습니다.


공유 메모리 제어: shmctl(2)

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

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: 공유 메모리 세그먼트의 잠금을 해제한다.


공유 메모리 사용 예제


부모/자식 프로세스 간 공유 메모리 사용 예제

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

#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>

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");
            for(i=0; i<10; i++) // 공유메모리에 'a'~'j'가지 문자 10개 기록
                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);   // shmctl 함수를 사용해 공유 메모리 제거
            break;
    }

    return 0;
}

[root@dev-web ch10]# ./ex10_5.out
Child Process =====
Parent Process =====
a b c d e f g h i j


실행 결과를 보면 부모 프로세스가 데이터를 제대로 받아서 출력했음을 알 수 있습니다.



독립적인 프로세스 간 공유 메모리 사용 예제

독립적인 두 프로세스를 서버(listener), 클라이언트(talker)로 역할을 나누어 공유 메모리를 통해 데이터를 주고받도록 한 예제입니다.


먼저 서버 역할을 하는 listener에서는 공유 메모리를 생성하고, talker가 공유 메모리에 데이터를 기록하고 시그널을 보낼 때까지 기다립니다. 시그널을 받으면 공유 메모리에서 데이터를 읽어오고, 응답 데이터를 공유 메모리에 기록합니다.

#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>

void handler(int dummy) {
    ;
}

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("Listenser wait for Talker\n");
    sigsuspend(&mask);

    // 시그널을 받으면 공유 메모리를 연결하고,
    // talker가 공유 메모리에 저장한 데이터를 읽어서 출력한다.
    printf("Listener Start =====\n");
    shmaddr = shmat(shmid, NULL, 0);
    strcpy(buf, shmaddr);
    printf("Listener recieved : %s\n", buf);

    // 다시 공유 메모리에 응답 데이터를 저장한 후 잠시 sleep 함수를
    // 실행한다. 공유 메모리의 연결을 끊기 전에 sleep 함수를 실행하는 이유는
    // talker가 ipcs를 실행할 시간을 주기 위해서다.
    strcpy(shmaddr, "Have a nice day\n");
    sleep(3);
    // 공유 메모리와의 연결을 해제한다.
    shmdt(shmaddr);

    return 0;
}


클라이언트 역할을 하는 talker에서는 listener가 생성한 공유 메모리의 식별자를 읽어서 공유 메모리를 연결하고 데이터를 기록한 후 listener에 시그널을 보냅니다. 잠시 기다렸다가 listener가 기록한 데이터를 읽어서 출력하고 공유 메모리를 해제한 후 삭제합니다.

#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>

int main(int argc, char **argv) {
    key_t key;
    int shmid;
    void *shmaddr;
    char buf[1024];

    // talker는 listener와 같은 파일과 정수값을 사용해 키를 생성한다.
    key = ftok("shmfile", 1);
    // 키값으로 listenr가 만든 공유 메모리의 식별자를 읽어온다.
    shmid = shmget(key, 1024, 0);

    // 공유 메모리와 연결하고 해당 메모리에 인삿말을 복사한다.
    shmaddr = shmat(shmid, NULL, 0);
    strcpy(shmaddr, "Hello, I'm, talker\n");

    // 명령행 인자로 받은 listener의 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;
}

먼저 listenr를 백그라운드로 실행한 후 talker를 실행합니다. 이는 talker를 실행할 때는 listener 프로세스의 ID를 명령행 인자로 지정해야 하기 때문입니다. listener는 talker가 시그널을 보낼때까지 기다렸다가 데이터를 읽어 출력합니다.


talker는 실행할 때 listener의 PID를 명령행 인자로 받습니다. talker는 listener가 보낸 데이터를 읽어서 출력하고 공유 메모리의 상태 정보를 출력합니다. 아직 listener가 공유 메모리와의 연결을 해제하지 않았으므로 NATTCH 값은 2가 됩니다. 최종적으로 ipcs 명령으로 확인해보면 공유 메모리가 제거되었음을 알 수 있습니다.


[root@dev-web ch10]# ./ex10_6s.out &
[1] 19398
[root@dev-web ch10]# Listenser wait for Talker

[root@dev-web ch10]# ./ex10_6c.out 19398
Listener Start =====
Listener recieved : Hello, I'm, talker

Listener said : Have a nice day


------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0xffffffff 0          root       644        1024       2

[root@dev-web ch10]# ipcs

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

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status

------ Semaphore Arrays --------
key        semid      owner      perms      nsems

[1]+  Done                    ./ex10_6s.out



04. 세마포어

세마포어는 프로세스 사이의 동기를 맞추는 기능을 제공합니다. 예를 들어, 공유 메모리에 여러 프로세스가 동시에 쓰기를 시도한다면 데이터가 손상되는 현상이 발생합니다. 따라서 여러 프로세스 사이의 동작 순서를 지정해야 합니다. 프로세스들이 공유 영역에 대한 접근 순서를 정하는 방법 중 하나가 세마포어 입니다. 이 절에서는 기본 개념을 살펴보고 세마포어 관련 함수와 예제를 다루겠습니다.



세마포어의 기본 개념


세마포어는 한 번에 한 프로세스만 작업을 수행하는 부분에 접근해 잠그거나(lock), 다시 잠금을 해제하는 기능을 제공하는 정수형 변수입니다. 이 정수형 변수는 함수를 통해 값을 변경합니다. 보통 세마포어라는 개념을 처음 제안한 네덜란드의 에츠허르 데이크스트라가 사용한 용어에 따라 잠금 함수는 p로 표시하고 잠금 해제 함수는 v로 표시합니다.


세마포어의 기본 동작 구조

세마포어는 중요한 처리 부분(critical section)에 들어가기 전에 p 함수를 실행해 잠금 기능을 수행하고, 처리를 마치면 다시 v 함수를 실행해 잠금을 해제합니다. 잠금 기능을 수행 중인 동안에는 다른 프로세스가 처리 부분의 코드를 실행할 수 없습니다. sem은 세마포어 값을 의미합니다.

p(sem); /* 잠금 */
// 중요한 처리 부분(critical section)
v(sem); /* 잠금 해제 */


p 함수의 기본 동작 구조

p 함수의 기본적인 동작은 다음과 같습니다.

p(sem) {
    while sem = 0 do wait;
    sem 값을 1 감소;
}

초기 sem 값은 1입니다. p 함수는 sem이 0이면 다른 프로세스가 처리 부분을 수행하고 있다는 의미이므로, 값이 1이 될 때까지 기다려야 합니다. sem이 0이 아니면 0으로 만들어 다른 프로세스가 들어오지 못하게 합니다.


v 함수의 기본 동작 구조

v(sem) {
    sem 값을 1 증가;
    if(대기 중인 프로세스가 있으면)
        대기 중인 첫 번째 프로세스를 동작시킨다.
}

v 함수의 기본 동작은 sem을 1만큼 증가시키고, 처리 부분을 수행하려고 대기 중인 프로세스가 있으면 첫 번째 대기 프로세스를 동작시킵니다.



세마포어 관련 함수


세마포어와 관련해 세마포어를 생성하는 함수, 생성된 세마포어를 이용해 잠금과 해제 등의 동작을 수행하는 함수, 세마포어 제어 기능을 수행하는 함수가 제공됩니다.


세마포어 생성: semget(2)

세마포어를 사용하려면 세마포어 식별자를 생성해야 합니다. 세마포어 식별자는 semget 함수를 사용해 생성합니다.

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

int semget(key_t key, int nsems, int semflg);
  • key: IPC_PRIVATE 또는 ftok 함수로 생성한 키
  • nsems: 생성할 세마포어 개수
  • semflg: 세마포어 접근 속성

semget 함수는 인자로 키와 생성할 세마포어 개수, 플래그를 받고 세마포어 식별자를 리턴합니다. 첫 번째 인자인 key에는 IPC_PRIVATE나 ftok 함수로 생성한 키를 지정합니다. 두 번째 인자인 nsems에는 생성할 세마포어의 개수를 지정합니다. 세마포어는 집합 단위로 처리되므로, 한 식별자에 여러 세마포어가 생성됩니다. 세번째 인자인 semflg에는 플래그와 접근 권한을 지정합니다. 플래그는 msgget 함수와 마찬가지로 IPC_CREAT와 IPC_EXCL을 사용합니다.


세마포어 식별자와 관련된 세마포어와 데이터 구조체가 새로 생성되는 경우는 다음 두가지입니다.


  • key가 IPC_PRIVATE다.
  • key가 0이 아니며 다른 식별자와 관련되어 있지 않고, 플래그(semflg)에 IPC_CREAT가 설정되어 있다.


이 두 가지 경우가 아니면 semget 함수는 기존 세마포어의 식별자를 리턴합니다.


세마포어에 관한 정보를 담고 있는 구조체는 semid_ds로, <sys/sem.h>에 다음과 같이 정의되어 있습니다.

struct semid_ds {
    struct ipc_perm     sem_perm;       // IPC 공통 구조체를 의미한다.
    // 세마포어 집합에서 첫번째 세마포어의 주소다.
    // sem 구조체는 세마포어 집합에 있는 각 세마포어에 관한 정보를 저장한다.
    // sem 구조체는 <sys/sem_impl.h> 파일에 정의되어 있다.
    struct sem          *sem_base;
    ushort_t            sem_nsems;      // 세마포어 집합의 세마포어 개수다.
    time_t              sem_otime;      // 세마포어 연산을 마지막으로 수행한 시각이다.
    int32_t             sem_pad1;
    time_t              sem_ctime;      // 세마포어의 접근 권한을 마지막으로 변경한 시각이다.
    int32_t             sem_pad2;
    int                 sem_binary;     // 세마포어의 종류를 나타내는 프래그다.
    long                sem_pad3[3];
};

struct sem {
    ushort_t            semval;         // 세마포어 값
    pid_t               sempid;         // 세마포어 연산을 마지막으로 수행한 프로세스 ID
    ushort_t            semncnt;        // 세마포어 값이 현재 값보다 증가하기를 기다리는 프로세스 수
    ushort_t            semzcnt;        // 세마포어 값이 0이 되기를 기다리는 프로세스 수
    kcondvar_t          semncnt_cv;
    kcondvar_t          semzcnt_cv;
};

새로운 세마포어 식별자를 리턴할 때 구조체는 다음과 같이 설정됩니다.


  • sem_perm.cuid, sem_perm.uid: 함수를 호출한 프로세스의 유효 사용자 ID로 설정된다.
  • sem_perm.cgid, sem_perm.gid: 함수를 호출한 프로세스의 유효 그룹 ID로 설정된다.
  • sem_perm.mode: semflg 값으로 설정된다.
  • sem_nsems: nsems 값으로 설정된다.
  • sem_otime: 0으로 설정된다.
  • sem_ctime: 현재 시각으로 설정된다.



세마포어 제어: semctl(2)

semctl 함수로 세마포어의 기능을 제어할 수 있습니다.

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

int semctl(int semid, int semnum, int cmd, ...);
  • sedmid: semget 함수로 생성한 세마포어 식별자
  • semnum: 기능을 제어할 세마포어 번호
  • cmd: 수행할 제어 명령
  • ...: 제어 명령에 따라 필요시 사용할 세마포어 공용체의 주소(선택 사항)


semctl 함수는 semid로 식별되는 세마포어 집합에서 semnum으로 지정한 세마포어에 cmd로 지정한 제어 기능을 수행합니다. cmd에 따라 선택적으로 네번째 인자가 있을 수 있습니다.


세 번째 인자인 cmd에 지정할 수 있는 값은 다음과 같습니다.


  • IPC_RMID: semid로 지정한 세마포어와 관련된 데이터 구조체를 제거한다.
  • IPC_SET: 세마포어 정보 내용 중 sem_perm.uid, sem_perm.gid, sem_perm.mode 값을 네 번째 인자로 지정한 값으로 변경한다. 이 명령은 root 권한이 있거나 유효 사용자 ID일 경우만 사용할 수 있다.
  • IPC_STAT: 현재 세마포어의 정보를 arg.buf로 지정한 메모리에 저장한다.
  • GETVAL: 세마포어의 semval 값을 읽어온다.
  • SETVAL: 세마포어의 semval 값을 arg.val로 설정한다.
  • GETPID: 세마포어의 sempid 값을 읽어온다.
  • GETNCNT: 세마포어의 semncnt 값을 읽어온다.
  • GETZCNT: 세마포어의 semzcnt 값을 읽어온다.
  • GETALL: 세마포어 집합에 있는 모든 세마포어의 semval 값을 arg.array가 가리키는 배열에 저장한다.
  • SETALL: 세마포어 집합에 있는 모든 세마포어의 semval 값을 arg.array가 가리키는 배열값으로 설정한다.


semctl 함수의 리턴값은 cmd에 따라 달라집니다. cmd가 GETVAL이면 semval 값을 리턴하고, GETPID면 sempid 값을 리턴합니다. cmd가 GETNCNT면 semncnt 값을 리턴하고, GETZCNT면 semzcnt 값을 리턴합니다. semctl 함수는 수행을 성공하면 0을, 오류가 발생하면 모든 경우에 -1을 리턴합니다.


네 번째 인자는 제어 명령에 따른 선택 사항입니다. 네 번째 항목이 필요할 경우 다음과 같은 공용체(union)을 사용합니다. 이 공용체는 프로그램에서 명시적으로 선언하고 사용해야 합니다.

union semun {
    int             val;
    struct semid_ds *buf;
    ushort_t        *array;
} arg;


세마포어 연산: semop(2)

잠금과 해제와 같은 세마포어 연산을 수행하려면 semop 함수를 사용합니다.

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

int semop(int semid, struct sembuf *sops, size_t nsops);
  • sedmid: semget 함수로 생성한 세마포어 식별자
  • sops: sembuf 구조체의 주소
  • nsops: sops가 가리키는 구조체의 크기

semop 함수는 semid가 가리키는 세마포어에 크기가 nsops인 sembuf 구조체로 지정한 연산을 실행합니다. sembuf 구조체는 <sys/sem.h> 파일에 정의되어 있습니다.

struct sembuf {
    ushort_t    sem_num;    // 세마포어 번호를 의미
    short       sem_op;     // 세마포어 연산을 의미
    // 연산을 위한 플래그로, IPC_NOWAIT 또는 SEM_UNDO를 지정한다.
    // SEM_UNDO: 프로세스가 비정상적으로 갑자기 종료할 때
    //          세마포어 동작을 취소한다.
    short       sem_flg;
};


세마포어 연산은 sembuf 구조체의 sem_op 항목에 지정합니다. sem_op 항목은 semop 함수가 수행할 기능을 정수로 나타내며, 다음과 같은 세 가지 경우가 있습니다.

if(sem_op < 0) {    /* 세마포어 잠금 */
    wait until semval >= | sem_op |;
    semval -= | sem_op |;
} else if (sem_op > 0) /* 세마포어 잠금 해제 */
    semval += sem_op;
else
    wait until semval is 0;

1) sem_op가 음수면 세마포어 잠금 기능을 수행한다. 이는 공유 자원을 사요하려는 것이다.

- semval 값이 sem_op의 절댓값과 같거나 크면 semval 값에서 sem_op의 절댓값을 뺀다.

- semval 값이 sem_op 값보다 작고, sem_flg에 IPC_NOWAIT이 설정되어 있으면 semop 함수는 즉시 리턴한다.

- semval 값이 sem_op 값보다 작은데 sem_flg에 IPC_NOWAIT이 설정되어 있지 않으면, semop 함수는 semncnt 값을 증가시키고 다음 상황을 기다린다.

  semval 값이 sem_op의 절댓값보다 같거나 커진다. 이 경우 semncnt 값은 감소하고 semval 값에서 sem_op의 절댓값을 뺀다.

 시스템에서 semid가 제거된다. 이 경우 errno가 EIDRM으로 설정되고 -1을 리턴한다.

 semop 함수를 호출한 프로세스가 시그널을 받는다. 이 경우 semncnt 값은 감소하고 시그널 처리 함수를 수행한다.

2) sem_op가 양수면 이는 세마포어의 잠금을 해제하고, 사용 중이던 공유 자원을 돌려준다. 이 경우 sem_op 값이 semval 값에 더해진다.

3) sem_op 값이 0인 경우

 - semval 값이 0이면 semop 함수는 즉시 리턴한다.

 - semval 값이 0이 아니고, sem_flg에 IPC_NOWAIT이 설정되어 있으면 semop 함수는 즉시 리턴한다.

 - semval 값이 0이 아니고, sem_flg에 IPC_NOWAIT이 설정되어 있지 않으며, semop 함수는 semzcnt 값을 증가시키고 semval 값이 0이 되길 기다린다.



세마포어 사용 예제


세마포어를 사용해 여러 프로세스 사이에서 동기화 작업을 수행하는 예제입니다. 전체 프로그램은 하나지만 길고 복잡하기에 각 기능별로 나누어 설명하겠습니다.


우선 initsem 함수를 사용해 세마포어를 생성하고 초기화하는 부분입니다.

#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

// 세마포어 공용체를 정의한다.
union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

int initsem(key_t semkey) {
    union semun semunarg;
    int status = 0, semid;

    // 인자로 받은 키를 지정해 세마포어 식별자를 생성한다.
    // 리턴값이 -1이고, errno 값이 EEXIST면 이미 존재하는
    // 세마포어 식별자라는 의미이므로 기존 식별자를 읽어온다.
    semid = semget(semkey, 1, IPC_CREAT | IPC_EXCL | 0600);
    if(semid == -1) {
        if (errno == EEXIST)
            semid = semget(semkey, 1, 0);
    } else {
        // semctl 함수를 사용해 세마포어 값을 1로 초기화한다.
        semunarg.val = 1;
        status = semctl(semid, 0, SETVAL, semunarg);
    }

    if(semid == -1 || status == -1) {
        perror("initsem");
        return (-1);
    }

    return semid;
}

int semlock(int semid){
    // semlock 함수는 sem_op 값을 -1로 설정해 공유 자원을 얻고 잠금 기능을 수행한다.
    // 세마포어 값의 초깃값을 1로 설정했으므로, 처음 semlock 함수를 실행하는 프로세스는
    // 세마포어 값에서 1을 빼면 0이 되어 잠금 기능을 수행한다.
    struct sembuf buf;

    buf.sem_num = 0;
    buf.sem_op = -1;
    // sem_flg를 UNDO로 설정해 문제가 발생하면 동작 취소 가능하게 한다.
    buf.sem_flg = SEM_UNDO;
    if(semop(semid, &buf, 1) == -1) {
        perror("semlock failed");
        exit(1);
    }
    return 0;
}

int semunlock(int semid) {
    // semunlock 함수는 sem_op 값을 1로 설정해 사용 중인 공유 자원의 잠금 기능을
    // 해제하고 되돌려주려 한다. 처음 semunlock 함수를 호출하면 세마포어 값이 0이므로
    // 여기에 1을 더하면 세마포어 값이 양수가 되어 잠금을 해제한다.
    struct sembuf buf;

    buf.sem_num = 0;
    buf.sem_op = 1;
    // sem_flg를 UNDO로 설정해 문제가 발생하면 동작 취가 가능하게 한다.
    buf.sem_flg = SEM_UNDO;
    if(semop(semid, &buf, 1) == -1) {
        perror("semunlock failed");
        exit(1);
    }
    return 0;
}

// semhandle 함수는 세마포어를 생성해 잠금을 수행하고
// 작업한 후 다시 잠금을 해제한다.
void semhandle() {
    int semid;
    pid_t pid = getpid();

    if((semid = initsem(1)) < 0)
        exit(1);

    semlock(semid);

    // /// 여기서는 간단한 출력문이나 실제로는 중요한 처리 부분이 오게 된다.
    printf("Lock : Process %d\n", (int)pid);
    printf("** Lock Mode : Critical Section\n");
    sleep(1);
    printf("Unlock : Process %d\n", (int)pid);

    semunlock(semid);

    exit(0);
}

// main 함수에서는 fork 함수를 세 번 호출해 자식 프로세스 세 개를 생성하고
// 각 자식 프로세스는 semhandle 함수를 실행한다. 즉, 자식 프로세스가 모두
// semhandle 함수를 실행하게 된다.
int main(void) {
    int a;
    for(a = 0; a < 3; a++)
        if(fork() == 0) semhandle();

    return 0;
}

[root@dev-web ch10]# Lock : Process 20217
** Lock Mode : Critical Section
Unlock : Process 20217
Lock : Process 20218
** Lock Mode : Critical Section
Unlock : Process 20218
Lock : Process 20219
** Lock Mode : Critical Section
Unlock : Process 20219


예제 실행을 마치면 ipcs 명령으로 세마포어의 정보를 확인하고, ipcrm 명령(ipcrm -s#)으로 삭제하도록 합니다.


[root@dev-web ch10]# ipcs

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

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status

------ Semaphore Arrays --------
key        semid      owner      perms      nsems
0x00000001 196608     root       600        1

[root@dev-web ch10]# ipcrm -s196608
[root@dev-web ch10]# ipcs

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

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status

------ Semaphore Arrays --------
key        semid      owner      perms      nsems