[리눅스] 공유메모리 (Shared Memory) 개념과 예제(shmget, shmat, shmdt, shmctl)
컴퓨터/운영체제(주로 리눅스) 2020. 4. 10. 00:15공유 메모리, 메시지큐 등 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.
https://reakwon.tistory.com/233
공유메모리(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_CREAT과 IPC_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이든 메모리를 동시에 접근할때 처리를 해주어야하며 이 포스팅에서는 그러한 처리는 하지 않았습니다.