10.시스템 V의 프로세스간 통신
10-1.개요
유닉스는 처음 개발된 이래 다양한 형태로 발전했다.
크게 BSD 계열과 시스템 V 계열로 구분할 수 있다.
시스템V계열 유닉스에서 개발해 제공하는 프로세스간 통신 방법이 메세지 큐, 공유 메모리, 세마포어 이다.
이 세가지를 묶어서 시스템V IPC 라고 한다. 이것은 SVR2에서 처음 개발되었고, SVR4에서도 제공하고 있으며 현재는 대부분의 유닉스 시스템에서 제공하고 있다.
시스템V IPC를 사용하려면 IPC의 객체를 생성해야한다.
이를 위해 공통으로 사용하는 기본요소가 키와 식별자(ID, IDentifier)이다.
또한 현재 사용중인 각 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 msgkey] [-Ssemkey] |
Message queue는 파이프와 유사하다.
단, 파이프는 스트립 기반으로 동작하고, 메세지 큐는 메세지(또는 패킷)단위로 동작한다.
메세지 큐는 우편함과 비슷하다고 생각하면 된다.
우편함처럼 메세지 큐를 만든 후 이를 통해 메세지를 주고 받는다.
- 메세지 큐 관련 함수
기능 | 함수원형 |
---|---|
메시지 큐 생성 | 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); |
공유 메모리(shared memory)는 한 프로세스의 일부분을 다른 프로세스와 공유하는 것을 의미한다. 메모리의 일부 공간을 두 독립적인 프로세스에서 공유하고 해당 메모리를 통해 데이터를 주고 받을 수 있다.
- 공유 메모리 관련 함수
기능 | 함수원형 |
---|---|
공유 메모리 생성 | 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); |
semaphore 는 프로세스 사이의 synchronization 을 맞추는 기능을 제공한다.
공유 메모리에 여러 프로세스가 동시에 쓰기를 시도하면 데이터가 손상되는 현상이 발생한다.
따라서 여러 프로세스 사이에서 동작의 순서를 지정해줘야하는데,
바로 세마포어가 프로세스 간의 공유 영역에 대한 접근 순서를 정하는 방법을 제공한다.
- 세마포어 함수
기능 | 함수원형 |
---|---|
세마포어 생성 | 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); |
10-2.시스템V IPC의 공통요소
시스템 V IPC에서 공통적으로 사용하는 기본 요소는 키와 식별자이다.
메세지 큐, 공유 메모리, 세마포어를 이용하려면 식별자가 필요하다.
또한 시스템V IPC에서는 현재 사용중인 각 통신 방법의 상태를 확인하고,
불필요한 것은 정리할 수 있는 관리 명령을 제공한다.
10-2-1. 키와 식별자
시스템 V IPC에서 사용하는 키는 key_t형으로 sys/types.h 에 선언되어 있으며, 32비트 정수형이다.
IPC를 사용하기 위해 새로운 IPC 객체를 생성할 때 키를 지정한다.
키가 다르면 서로 다른 객체가 생성된다.
각 IPC 방법의 객체 생성 함수에서 키를 받아 새로운 IPC 객체를 생성하고, 이를 식별하는 식별자를 리턴한다.
이 식별자를 사용해 통신할 수 있다.
- 키의 생성
시스템 V IPC에서 사용하는 키는 다음과 같이 생성할 수 있다.
* 키로 IPC_PRIVATE 를 지정하다. 식별자를 알아야 통신할 수 있으므로 IPC_PRIVATE를 키로 지정해 생ㄷ성된 식별자를 서버와 클라이언트 모두 알 수 있게 해야한다. fork함수로 생성된 부모-자식 프로세스 간 통신에서 유용하게 쓸 수 있다.
* ftok 함수로 키를 생성한다. 이 함수는 경로명과 숫자 값을 받아서 키를 생성한다. 따라서 서버와 클라이언트가 같은 경로명과 숫자를 지정하면 공통 식별자를 생성할 수 있다.
같은 키로 생성된 식별자는 통신에 사용할 수 있다.
따라서 미리 정해진 키를 서버와 클라이언트 프로세스가 공유할 수 있게 해야한다.
헤더 파일이나 환경 설정 파일에서 키를 저장해 공유할 수 있다.
단 이 키를 제 3의 프로세스가 먼저 사용하고 있으면 안된다.
- 키 생성하기 : ftok(3)
#include sys/ipc.h>
key_t ftok(cosnt char *path, int id);
* path: 파일 시스템에 이미 존재하는 임의의 파일 경로명
* id: 키값을 생성할 때 지정하는 임의의 번호 (1~256)
이 함수는 path에 지정한 경로명과 id에 지정한 정수값을 조합해 새로운 키를 생성한다.
경로명은 파일 시스템에 존재해야한다. 이 키는 IPC객체를 생성할 때 사용한다.
키를 구성하는 32비트 중 처음 12비트에는 stat구조체의 st_dev값, 다음 12비트에는 st_ino값이 저장된다.
마지막 8비트에는 id 인자의 정수값이 저장된다.
따라서 같은 경로 명과 같은 정수값을 지정하면 같은 식별자가 리턴된다.
경로명은 같지만 정수값이 다르면 다른 식별자가 리턴된다.
정수값이 같더라도 경로명이 다르면 다른 식별자가 리턴된다.
경로명으로 지정한 파일에 대해 stat함수를 실행할 수 있어야 한다.
키가 참조하고 있는 경로의 파일이 삭제되면 ftok함수는 오류를 발생시킨다.
삭제된 파일을 다시 생성하고 같은 숫자를 id로 지정한 후 ftok함수로 키를 생성해도 다른 키가 생성된다. indoe(st_ino)값이 달라지기 때문이다.
키의 마지막 8비트에 정수값이 저장되므로 1~255사이의 값을 지정해야한다.
이 값을 0으로 지정하면 ftok함수가 어떻게 동작할지 정의되어 있지 않으므로 사용하지 않는 것이 좋다.
ftok 함수는 다음과 같은 형태로 사용한다.
key_t key;
key = ftok("/export/home/keyfile", 1);
10-2-2. IPC 공통 구조체
시스템 V IPC를 사용하기 위해 해당 IPC 객체를 생성하면 IPC 공통 구조체가 정의된다.
시스템 V IPC에서 공통으로 사용하는 IPC 공통 구조체는 sys/ipc.h 파일에 다음과 같이 정의되어 있다.
struct ipc_perm{
uid_t uid;
gid_t gid;
uid_t cuid;
gid_t cgid;
mode_t mode;
uint_t seq;
key_t key;
int pad[4];
};
* uid : 구조체의 소유자 ID
* gid : 구조체의 소유 그룹 ID
* cuid: 구조체를 생성한 사용자ID
* cgid: 구조체를 생성한 그룹 ID
* mode : 구조체에 대한 접근 권한
* seq : 슬롯의 일련번호
* key : 키값
* pad : 향후 사용을 위해 예약되어 있는 영역
- IPC 구조체는 사용을 마치면 반드시 프로그램 안에서 삭제해야한다. 시스템에서 제공가능한 총 개수는 제한되어 있기 때문이다.
10-2-3. 시스템 V IPC 정보 검색
시스템 V IPC의 정보를 검색하고 현재 상태를 확인하는 명령은 ipcs이다.
ipcs 명령을 실행하는 동안에도 IPC 상태가 변경될 수 있다.
ipcs 명령은 검색하는 순간의 정확성만 보장한다.
- ipcs명령의 기본 형식
ipcs [-aAbciJmopqstZ] [-D mtype]
- ipcs 명령의 출력 형식
ipcs명령의 다양한 옵션을 적절히 조합해 원하는 정보를 검색할 수 있다.
아무 옵션 없이 ipcs 명령을 사용하면 현재 동작중인 메시지 큐와 공유 메모리, 세마포어에 대한 간단한 정보를 검색한다.
10-2-4. 시스템 V IPC 정보 삭제
시스템 V IPC 정보를 검색하고 이중 불필요한 IPC 객체를 삭제하려면 ipcrm 명령을 사용한다.
ipcrm [-m shmid][-s semid] [-M shmkey][-Q msgkey][-S semkey]
- -m shmid : shmid로 지정한 공유 메모리를 삭제한다. 공유 메모리에 대한 마지막 해제 동작 이후에 관련된 메모리 세그먼트와 데이터 구조체가 제거된다.
- -q msqid : msqid로 지정한 메시지 큐와 데이터 구조체를 삭제한다.
- -s semid : semid로 지정한 세마포어와 데이터 구조체를 삭제한다.
- -M shmkey : shmkey로 생성한 공유 메모리를 삭제한다. 공유 메모리에 대한 마지막 해제 동작 이후에 관련된 메모리 세그먼트와 데이터 구조체가 제거된다.
- -Q msgkey : msgkey로 생성한 메시지 큐와 데이터 구조체를 삭제한다.
- -S semkey : semkey로 생성한 세마포어와 데이터 구조체를 삭제한다.
10-3.메세지 큐
Message queue는 pipe와 비슷하다. 파이프는 스트림 기반으로 동작하고 메시지 큐는 메시지(패킷)단위로 동작한다. 각 메시지의 최대 크기는 제한되어 있다.
메시지 큐는 우편함과 비슷하다고 생각하면된다. 우편함처럼 메시지 큐를 만든 후 이를 통해 메시지를 주고 받는다.
각 메세지에는 메세지 유형이 있으므로 수신 프로세스는 어떤 유형의 메시지를 받을 것인지 선택할 수 있다.
10-3-1. 메세지 큐 관련 함수
메세지 큐 제어 함수는 메세지 큐를 제거하거나 상태 정보를 설정하고 읽어오는 등 메세지 큐에 대한 제어 기능을 수행한다.
- 메세지 큐 생성 : msgget(2)
메세지 큐를 사용하려면 메세지 큐 식별자를 생성해야 한다.
msgget함수를 사용하면 메시지 큐 식별자를 생성할 수 있다.
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
* key : 메시지 큐를 구별하는 키
* msgflg : 메시지 큐의 속성을 설정하는 플래그
- IPC_CREATE(0001000) : 새로운 키면 식별자를 새로 생성한다.
- IPC_EXCL(0002000) : 이미 존재하는 키면 오류가 발생한다.
msget 함수는 인자로 키와 플래그를 받아 메시지 큐 식별자를 리턴한다.
첫번째 인자인 key에는 IPC_PRIVATE나 ftok함수로 생성한 키를 지정한다.
두번째 인자인 msgflg에는 플래그와 접근권한을 지정한다.
사용가능한 플래그는 sys/ipc.h에 정의되어 있다.
메시지 큐 식별자와 관련된 메시지 큐와 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; //현재 메시지 큐에 있는 메세지의 총 바이트 수
msgqum_t msg_qnum; //메세지 큐에 있는 메세지의 개수
msglen_t msg_qbyte; //메세지 큐의 최대크기(바이트수)
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; //시간 정보를 확장하기 위한 예비공간2
time_t msg_ctime; //마지막으로 메시지 큐의 권한을 변경한 시각
int32_t msg_pad3; //시간정보 확장 예비공간3
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_lspid, msg_lrpid, msg_stime, msg_rtime : 0으로 설정된다.
- msg_ctime : 현재 시각으로 설정된다.
- msg_qbytes : 시스템의 제한값으로 설정된다.
msgget함수는 수행을 성공하면 메시지 큐 식별자를 음수가 아닌 정수로 리턴하고, 실패하면 -1을 리턴한다. msgget함수로 메시지 큐 식별자를 생성하는 예는 다음과 같다.
<메시지큐의 식별자를 새로 생성. 접근 권한은 0640(rw-r-----)>
key_t key;
int id
key = ftok("keyfile", 1);
id = msgget(key, IPC_CREAT|0640);
- 메시지 전송 : msgsnd(2)
#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)
이 함수는 msgget함수가 리턴한 메시지큐를 통해 크기가 msgsz인 메시지를 메세지 버퍼에 담아 전송한다.
msgflg에는 메시지 큐가 가득 찼을 때의 동작을 지정한다.
메시지를 담고 있는 메시지 버퍼는 msgbuf 구조체를 사용한다.
이 구조체는 sys/msg.h에 정의 되어 있으며 다음과 같다.
struct msgbuf {
long mtype;
char mtext[1];
};
* mtype : 메시지 유형으로 양수를 지정한다.
* mtext : msgnd 함수의 msgsz로 지정한 크기의 버퍼로 메시지 내용이 저장된다.
이 구조체는 메시지 큐를 생성할 때 앞의 형식대로 사용자가 정의해 사용한다.
- 메시지 수신 : msgrcv(2)
이 함수는 메시지 큐로 메시지를 수신하는데 사용한다.
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long int msgtyp, int msgflg);
* msqid : msgget함수로 생성한 메시지 큐 식별자
* msgp : 메시지를 담고 있는 메시지 버퍼의 주소
* msgsz : 메시지 버퍼의 크기
* msgtyp : 읽어올 메시지의 유형
- 0 : 메시지 큐의 다음메세지를 읽어온다.
- 양수 : 메시지 큐에서 msgtyp으로 지정한 유형과 같은 메시지를 읽는다.
- 음수 : 메시지 유형이 msgtyp으로 지정한 값의 절대값과 같거나 작은 메시지를 읽어온다.
* msgflg : 블록 모드(0)/비블록 모드(IPC_NOWAIT)
msqid가 가리키는 메시지 큐에서 msgtype이 지정하는 메시지를 읽어 msgp가 가리키는 메시지 버퍼에 저장한다. 메시지 버퍼의 크기는 msgsz에 저장하고, msgflg는 메시지 큐가 비었을 때 어떻게 동작할지 알려준다.
msgrcv의 수행이 성공하면 msquid_ds구조체에서 msg_qnum값이 1 감소하고,
msg_lrpid는 msgrcv 함수를 호출한 프로세스의 ID로 설정된다.
msg_rtime은 현재 시각으로 설정된다.
msgrcv 함수는 수행을 성공하면 읽어온 메시지의 바이트 수를 리턴하고 실패시 -1을 리턴하고 메시지를 읽어오지 않는다.
- 메시지 제어 : msgctl(2)
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
* msqid : msgget함수로 생성한 메시지 큐 식별자
* 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로 지정한 메모리에 저장한다.
* buf : 제어 기능에 사용되는 메시지 큐 구조체의 주소
* 성공시 0, 실패시 -1 리턴.
이 함수는 msquid로 지정한 메시지 큐에서 cmd에 지정한 제어를 수행한다.
buf는 cmd의 종류에 따라 제어값을 지정하거나 읽어오는데 사용한다.
10-4. 공유메모리
공유 메모리(Shared memory)는 같은 메모리 공간을 두개이상의 프로세스가 공유하는 것이다.
같은 메모리 공간을 사용하므로 이를 통해 데이터를 주고 받을 수 있다.
여러 프로세스가 메모리를 공유하고 있으므로 당연히 읽고 쓸 때 동기화가 필요하다.
공유 메모리를 동기화 하지 않을 경우 데이터가 손실될 수 있다.
10-4-1. 공유 메모리 관련 함수
공유 메모리를 생성하고, 생성된 공유 메모리와 연결하며, 공유 메모리를 해제하는 함수와 제어하는 함수 등이 있다.
메시지 큐 함수와 마찬가지로 공유 메모리 제거, 잠금 설정, 잠금 해제 등의 제어 기능을 수행한다.
- 공유 메모리 생성 : shmget(2)
공유 메모리를 사용하려면 공유 메모리 식별자를 생성해야 한다.
공유 메모리 식별자는 shmget 함수를 사용해 생성한다.
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
* key : IPC_PRIVATE 또는 ftok함수로 생성한 키
* size: 공유할 메모리의 크기
* shmflg : 공유 메모리의 속성을 지정하는 플래그
공유 메모리 식별자와 관련된 공유 메모리와 데이터 구조체가 새로 생성되는 경우는 다음과 같다.
- key가 IPC_PRIVATE이다.
- key가 0이 아니며, 다른 식별자와 관련되어 있지 않고, 플래그(msgflg)에 IPC_CREAT 가 설정되어 있다.
이 두가지 경우가 아니면 shmget함수는 기존 공유 메모리의 식별자를 리턴한다.
공유 메모리에 관한 정보를 담고 있는 구조체는 shmid_ds로 sys/shm.h에 다음과 같이 정의되어 있다.
struct shmid_ds {
struct ipc_perm shm_perm;
size_t shm_segsz;
struct anon_map *shm_amp;
pid_tt_t shm_lpid;
pit_t shm_lpid;
shmatt_t shm_nattch;
ulong_t shm_cnattch;
time_t shm_atime;
int32_t shm_pad1;
time_t shm_dtime;
int32_t shm_pad2;
time_t shm_ctime;
int32_t shm_pad3;
int32_t shm_pad4[4];
}
- 공유 메모리 연결 : shmat(2)
생성된 공유 메모리를 사용하려면 공유 메모리를 프로세스의 데이터 영역과 연결해야한다.
공유 메모리를 연결하려면 shmat함수를 사용해야한다.
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
* shmid : shmget 함수로 생성한 공유 메모리 식별자
* shaddr : 공유 메모리를 연결할 주소
* shmflg : 공유 메모리에 대한 읽기/쓰기 권한
shmget 함수로 생성한 공유 메모리의 식별자를 shmid에 지정하고, shmaddr에 공유메모리를 연결할 주소를 지정한다.
shmaddr에는 특별한 경우가 아니면 0을 지정한다.
값이 0이면 시스템이 알아서 적절한 주소에 공유 메모리를 연결한다.
shmflg는 플래그로 0이면 공유 메모리에 대해 읽기와 쓰기가 가능하고,
SHM_RDONLY며 읽기만 가능하다.
shmat 함수는 성공시 공유 메모리의 시작주소를 리턴한다.
- 공유 메모리 연결 해제 : shmat(2)
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(char *shmaddr);
*shmaddr : 연결을 해제할 공유 메모리의 시작주소
- 공유 메모리 제어 : shmctl(2)
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, sturct shmid_ds *buf);
* shmid : shmget함수로 생성한 공유 메모리 식별자
* cmd : 수행할 제어 기능
- IPC_RMID : shmid로 지정한 공유 메모리를 제거하고, 관련 데이터 구조체를 제거한다.
- IPC_SET : 공유 메모리의 정보 내용 중 shm_perm.uid, shm_perm.gid, shm_perm.mode 값을 세번 째 인자로 지정한 값으로 바꾼다. 이 명령은 root권한이 있거나 유효 사용자 ID인 경우만 사용할 수 있다.
- IPC_STAT : 현재 공유 메모리의 정보를 buf로 지정한 메모리에 저장한다.
- SHM_LOCK: 공유 메모리 세그먼트를 잠근다.
- SHM_UNLOCK : 공유 메모리 세그먼트의 잠금을 해제한다.
* buf : 제어 기능에 사용되는 공유 메모리 구조체의 주소
10-4-2. 공유 메모리 사용 예제
- 부모자식 프로세스 간 공유 메모리 사용 예제
#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;
//자식 프로세스가 수행하는 부분이다. 자식프로세스는 공유메모리를 연결하고 반복문으로 공유메모리에 a~j까지 문자 10개를 기록하고 공유메모리를 해제한다.
case 0:
shmaddr = (char *)shmat(shmid, (char *)NULL, 0);
printf("child process === \n");
for(i=0; i<10; i++){
shmaddr[i] = 'a'+i;
shmdt((car *)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명령으로 공유 메모리의 상태를 확인하기 위해서이다.
sleep(5);
//공유메모리의 연결을 해제한다.
shmdt((char *)shmaddr2);
//공유메모리를 제거한다.
shmctl(shmid, IPC_RMID (struct shmid_ds *)NULL);
break;
}
return 0;
}
* 실행 결과를 보면 부모 프로세스가 데이터를 제대로 받아서 출력했음을 알 수 있다.
* ipcs 명령을 실행하면 공유메모리가 생겼다가 프로세스가 종료된 후에는 삭제되는 것을 알 수 있다.
#ipcs -mo
- 독립적인 프로세스 간 공유 메모리 사용예제
다음 예제는 독립적인 두 프로세스를 서버(listener), 클라이언트(talker)로 역할을 나누어 공유 메모리를 통해 데이터를 주고 받도록 한 예제다.
먼저 서버 역할을 하는 listener에서는 공유 메모리를 생성하고, talker가 공유 메모리에 데이터를 기록하고 시그널을 보낼 때까지 기다린다.
시그널을 받으면 공유 메모리에서 데이터를 읽어오고, 응답 데이터를 공유 메모리에 기록한다.
<listener (서버역할)>
#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("Listener wait for Talker\n");
//SIGUSR1시그널을 받을 때까지 기다리도록 한다.
sigsuspend(&mask);
printf("Listener Start ===\n");
//시그널을 받으면 공유 메모리를 연결하고, talker가 공유 메모리에 저장한 데이터를 읽어서 출력한다.
shmaddr = shmat(shmid, NULL, 0);
strcpy(buf, shmaddr);
printf("Listener received : %s\n", buf);
//다시 공유 메모리에 응답 데이터를 저장한 후 잠시 sleep함수를 실행한다.
//공유 메모리의 연결을 끊기전에 sleep함수를 실행하는 이유는 talker가 ipcs를 실행할 시간을 주기 위해서이다.
strcpy(shmaddr, "have a nice day\n");
sleep(3);
//공유 메모리와의 연결을 해제한다.
shmdt(shmaddr);
return 0;
}
클라이언트talker
클라이언트 역할을 하는 talker에서는 listener가 생성한 공유 메모리의 식별자를 읽어서 공유 메모리를 연결하고 데이터를 기록한후 listener 시그널을 보낸다.
잠시 기다렸다가 listener가 기록한 데이터를 읽어서 출력하고 공유메모리를 해제한 후 삭제한다.
<talker(클라이언트 역할)>
#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);
//키값으로 listener가 만든 공유 메모리의 식별자를 읽어온다.
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 -mo");
//공유 메모리 연결을 해제한다.
shmdt(shmaddr);
//공유 메모리를 제거한다.
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
* 실행 명령
#listener &
#talker 4946
#ipcs
위 예제를 실행할 때 먼저 listener를 백그라운드로 실행한 후 talker를 실행한다.
talker를 실행할 때는 listener 프로세스의 ID를 명령행 인자로 지정해야하기 때문이다.
listener는 talker가 시그널을 보낼 때까지 기다렸다가 데이터를 읽어 출력한다.
talker는 실행할 때 listener의 PID를 명령행 인자로 받는다.
talker는 listener가 보낸 데이터를 읽어서 출력하고 공유 메모리의 상태 정보를 출력한다.
아직 listener가 공유 메모리와의 연결을 해제하지 않았으므로 NATTCH 값은 2가 된다.
최종적으로는 ipcs명령으로 확인해보면 공유 메모리가 제거되었음을 알 수 있다.
10-5. 세마포어
semaphore는 프로세스 사이의 synchornization 을 맞추는 기능을 제공한다.
예를 들어 공유 메모리에 여러 프로세스가 동시에 쓰기를 시도할 경우 데이터가 손상되는 현상이 발생한다.
따라서 여러 프로세스 사이의 순서를 지정해야 한다.
프로세스들이 공유 영역에 대한 접근 순서를 정하는 방법 중 하나가 세마포어이다.
10-5-1. 세마포어의 기본 개념
세마포어는 한번에 한 프로세스만 작업을 수행하는 부분에 접근해 잠그거나(lock), 다시 잠금을 해제하는 기능을 제공하는 정수형 변수이다.
이 정수형 변수는 함수를 통해 값을 변경한다.
보통 세마포어라는 개념을 처음 제한한 네덜란드의 에츠허르 데이크스트라가 사용한 용어에 따라 잠금 함수는 p 로 표시하고 잠금 해제 함수는 v로 표시한다.
세마포어의 기본 동작 구조
세마포어는 중요한 처리 부분(critical section)에 들어가기 전에 p 함수를 실행해 잠금 기능을 수행하고, 처리를 마치면 다시 v 함수를 실행해 잠금을 해제한다.
잠금 기능을 수행중인 동안에는 다른 프로세스가 처리 부분의 코드를 실행할 수 없다.
sem은 세마포어 값을 의미한다.
p(sem); // 잠금
critical section..
v(sem); //잠금해제
- 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만큼 증가시키고 처리 부분을 수행하려고 대기중인 프로세스가 있으면 첫번째 대기 프로세스를 동작시킨다.
10-5-2 세마포어 관련 함수
세마포어와 관련해 세마포어를 생성하는 함수 생성된 세마포어를 이용해 잠금과 해제등의 동작을 수행하는 함수, 세마포어 제어 기능을 수행하는 함수가 제공된다.
- 세마포어 생성 : semget(2)
세마포어를 사용하려면 세마포어 식별자를 생성해야한다. 세마포어 식별자는 semget함수를 사용해 생성한다.
#include <sys/type.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
* key: IPC_PRIVATE 또는 ftok 함수로 생성한 키
* nsems : 생성할 세마포어 개수
* semflg : 세마포어 접근 속성 (IPC_CREATE / IPC_EXCL)
세마포어는 집합 단위로 처리되므로 한 식별자에 여러 세마포어가 생성된다.
세마포어 식별자와 관련된 세마포어와 데이터 구조체가 새로 생성되는 경우는 다음과 같다.
* key가 IPC_PRIVATE.
* key가 0이 아니며 다른 식별자와 관련되어 있지 않고, 플래그(semflg)에 IPC_CREAT가 설정되어 있다.
이 두가지 경우가 아니면 기존 세마포어의 식별자를 리턴한다.
- 세마포어 구조체 semid_ds는 sys/sem.h에 정의되어 있다.
struct semid_ds {
struct ipc_perm sem_perm; //IPC 공통 구조체를 의미한다.
struct sem *sem_base; //세마포어 집합에서 첫번째 세마포어의 주소.
ushort_t sem_nsems; //세마포어 집합의 세마포어 개수
time_t sem_ctime; //세마포어 연산을 마지막으로 수행한시간
int32_t sem_pad1;
time_t sem_ctime; //세마포어의 접근 권한을 마지막으로 변경한 시간
int32_t sem_pad2;
int sem_binary; //세마포어의 종류를 나타내는 플래그
long sem_pad3[3];
};
struct sem {
ushort semval; //세마포어 값
pid_t sempid; //세마포어 연산을 마지막으로 수행한 프로세스의 ID
ushort_t semncnt; //세마포어 값이 현재값보다 증가하기를 기다리는 프로세스수
ushort_t semzcnt; //세마포어 값이 0이 되기를 기다리는 프로세스 수
kcondvar_t semncnt_cv;
kcondvar_t semzcnt_cv;
};
- 세마포어 제어 : semctl(2)
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
*semid: semget함수로 생성한 세마포어 식별자
*semnum: 기능을 제어할 세마포어 번호
*cmd : 수행할 제어 명령
* ...: 제어 명령에 따라 필요시 사용할 세마포어 공용체의 주소
<4번째 파라미터에서 사용하는 공용체(union)>
union semun{
int val;
struct semid_ds *ds;
ushort_t *array;
} arg;
semctl함수는 semid로 식별되는 세마포어 집합에서 semnum으로 지정한 세마포어에 cmd로 지정한 제어 기능을 수행한다. 이 함수의 리턴 값은 cmd에 따라 달라진다.
- 세마포어 연산 : semop(2)
잠금과 해제와 같은 연산을 수행하려면 semop함수를 사용한다.
#include <sys/type.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
* semid : semget함수로 생성한 세마포어 식별자
* sops : sembuf 구조체의 주소
* nsops : sops가 가리키는 구조체의 크기
semop함수는 semid가 가리키는 세마포어에 크기가 nsops인 sembuf 구조체로 지정한 연산을 실행한다. 이 구조체는 sys/sem.h에 정의되어 있다.
struct sembuf{
ushort_t sem_num; //세마포어 번호
short sem_op; //세마포어 연산
short sem_flg; //연산을 위한 플래그. IPC_NOWAIT/SEM_UNDO를 지정한다.
// SEM_UNDO: 프로세스가 갑자기 비정상종료시, 세마포어 동작을 취소함
};
세마포어 연산은 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;
sem_op가 음수면 세마포어 잠금기능을 수행한다. 이는 공유 자원을 사용하려는 것이다.
sem_op가 양수면 이는 세마포어의 잠금을 해제하고, 사용중이던 공유 자원을 돌려준다. sem_op값이 semval값에 더해진다.
sem_op값이 0인 경우
- semval값이 0이면 semop함수는 즉시 리턴한다.
- semval != 0 and sem_flg = IPC_NOWAIT ==> semop함수는 즉시 리턴한다.
- semval != 0 and sem_flg != IPC_NOWAIT ==> semop함수는 semzcnt값 증가 및 semval값이 0이 되길 기다린다.
10-5-3 세마포어 사용 예제
- 세마포어를 사용해 여러 프로세스 사이에서 동기화 작업을 수행하는 예제
<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 shourt *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(errono == EEXIST)
semid = semget(semkey, 1, 0);
}
else {
semunarg.val = 1;
//이 함수를 사용해 세마포어 값을 1로 초기화한다.
status = semctl(semid, 0, SETVAL, semuarg);
}
if(semid == -1 || status == -1){
perror("initsem");
return (-1);
}
return semid;
}
<세마포어 연산>
int semlock(int semid){
struct sembuf buf;
buf.sem_num = 0;
//sem_op값을 -1로 설정해 공유 자원을 얻고, 잠금 기능을 수행한다.
//세마포어의 초기값을 1로 설정했으므로, 처음 semlock함수를 실행하는 프로세스는 0이 되어 잠금 기능을 수행한다.
buf.sem_op = -1;
buf.sem_flg = SEM_UNDO;
if(semop(semid, &buf, 1) == -1) {
perror("semlock failed");
exit(1);
}
return 0;
}
//semunlock 함수는 sem_op 값을 1로 설정해 사용중인 공유 자원의 잠금 기능을 해제하고 되돌려주려고 한다. 처음 이 함수를 호출하면 세마포어값이 0에서 1을 더해 양수가 되어 잠금이 해제된다.
int semunlock(int semid){
struct sembuf buf;
buf.sem_num = 0;
buf.sem_op = 1;
buf.sem_flg = SEM_UNDO;
if(semop(semid, &buf, 1) == -1){
perror("semunlock failed");
exit(1);
}
return 0;
}
//sem_flg를 SEM_UNDO로 설정해 문제가 발생하면 동작을 취소할 수 있도록 한다.
<세마포어 생성 함수를 호출해 세마포어를 생성한 다음, 잠금 함수를 호출해 잠근후 작업을 수행하고 해제함수를 호출해 잠금을 해제하는 함수>
//semhandle함수는 initsem함수를 통해 세마포어를 생성해 잠금을 수행하고(semlock함수), 작업한 후 다시 잠금을 해제한다(semunlock).
void semhandle(){
int semid;
pid_t pid = getpid();
if((semid = initsem(1)) < 0)
exit(1);
semlock(semid);
//printf문이 실제 수행 부분으로, 실제 상황에서는 중요한 처리 부분이 온다.
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함수를 같이 실행하게 된다.
//semlock함수 실행 부분과 semunlock 실행 부분 없이 실행하면 lock이 모두 출력된 후 unlock이 출력된 것을 알 수 있다. 한 프로세스가 pritnf 구문을 실행하는 동안 다른 프로세스도 이 부분을 실행했기 때문이다.
int main(void){
int a;
for(a = 0; a <3; a++)
if(fork() == 0) semhandle();
return 0;
}
* 예제 실행을 마치면 ipcs 명령으로 세마포어의 정보를 확인하고, ipcrm명령(ipcrm -s#)으로 삭제하도록 한다.