[LinuxProgramming] Chapter 10. 메모리 관리
리눅스 프로그래밍 원리와 실제 - 창병모 교수님
10.1 변수와 메모리
1) 프로세스
- 프로세스
- 프로세스는 실행 중인 프로그램
- 프로그램 실행을 위해서는 프로그램의 코드, 데이터, 스택, 힙, U-영역 등이 필요
- 프로세스 이미지(구조)는 메모리 내의 프로세스 레이아웃
- 프로그램 자체가 프로세스는 아님
- 프로세스 구조
- 코드 세그먼트
- 기계어 명령어
- 데이터 세그먼트
- static, 전역변수가 저장됨
- 스택(stack)
- 함수 호출과 관련
- 지역 변수, 매개 변수, 반환 주소, 반환값 등
- 잦은 함수 호출 시 메모리 공간 부족으로 stack overflow 발생
- 힙(heap)
- 동적 메모리 할당
- malloc() in C
- new class() in java
2) 할당 방법에 따른 변수들의 분류
- 자동 변수: C언어에서 함수 호출/복귀에 따라 자동으로 할당/해제되는 변수
- 프로세스의 스택 영역에 순서대로 할당
- 지역 변수, 매개변수
- 정적 변수: static으로 선언된 변수들로 이 변수들을 위한 메모리는 프로그램이 시작될 때 할당되어 프로그램이 끝날 때 해제됨
- 프로세스의 데이터 영역에 할당
- 전역 변수, static 변수
- 동적 변수: 필요한만큼만 메모리를 요청해서 사용하는 것으로 메모리를 절약할 수 있음
- 프로세스의 힙 영역에 할당
3) 다양한 변수 선언 예제
#include <stdio.h> #include <stdlib.h> int a = 1;
static int b = 2;
int main() { int c = 3;
static int d = 4; char *p;
p = (char *) malloc(40);
fun(5); }
void fun(int n)
{
int m = 6;
... }
1. 데이터 영역: 변수 a는 전역변수이며 b와 d는 정적 변수들이므로 프로그램이 시작되면 데이터 영역에 저장됨. 특히 변수 d는 지역 변수지만 static으로 선언된 정적 변수이므로 데이터 영역에 저장됨
2. 스택: 변수 c와 p는 지역 변수로 main()함수가 실행될 때 같이 자동적으로 스택에 메모리가 할당되어 스택에 저장됨. 또한 main() 함수에서 fun() 함수를 호출하게 되면 fun() 함수의 매개변수 n과 지역 변수 m을 위한 메모리 할당이 이루어짐. fun() 함수의 실행을 마치고 반환하게 되면 이들 변수들을 위한 메모리는 해제됨
* 자동 변수는 auto키워드를 사용하긴 했으나, 지역 변수의 경우 기본적으로 자동 변수로 인식하기 때문에 키워드를 생략
3. malloc() 함수를 호출하면 40바이트의 메모리 공간이 힙 영역에 동적으로 할당되고 포인터 변수 p가 이 메모리 공간을 가리킴
10.2 동적 메모리 할당
1) 메모리 할당
- 힙에 동적 메모리 할당
- 라이브러리가 메모리 풀을 관리
- 동적 할당을 하는 이유: 필요할 때 필요한 만큼만 메모리를 요청해서 사용하여 메모리를 절약
2) 동적 할당 malloc()
#include<stdlib.h>
void *malloc(size_t size);
// size 크기의 메모리를 할당하며 그 시작주소를 void* 형으로 반환
// 실제로 어떤 포인터 변수에 할당하기 위해서는 먼저 해당되는 포인터 자료형을 형 변환해 주어야 함
void free(void *ptr);
// 포인터 p가 기리키는 메모리 공간을 해제
3) 배열 할당 calloc()
- malloc() 함수는 할당할 메모리의 크기를 계산하여 넘겨주어야 함. 이와 같이 같은 크기의 메모리를 여러 개를 할당하는 경우에는 calloc() 함수를 사용하면 보다 쉽게 할 수 있음
#include<stdlib.h>
void *calloc(size_n, size_t size);
// 크기가 size인 메모리 공간을 n개 할당
// 값을 모두 0으로 초기화
// 실패하면 NULL을 반환
4) 할당된 메모리 크기 조정 realloc()
- realloc(): 할당된 메모리의 크기를 늘리거나 줄이는데 사용
#include<stdlib.h>
void *realloc(void *ptr, size_t newsize);
// ptr이 가리키는 이미 할당된 메모리의 크기를 newsize로 변경
4) 스택에 메모리 할당
- alloca(): 동적 메모리를 스택에 할당하기 위해 사용
#include<alloca.h>
void *alloca(size_t size);
10.3 동적 할당과 연결리스트
- 연결 리스트: 같은 타입의 데이터 항목 여러 개를 포인터를 통해 연결하는 자료구조
- 자기 참조 구조체: 연결리스트를 유지하기 위해서는 구조체 내에 자기와 같은 타입의 구조체를 가리킬 수 있는 포인터 변수를 선언하는 구조체
- 학생 정보를 저장하는 구조체 및 동적 할당
struct student{
int id;
char name[20];
struct student *next;
};
10.4 공유 메모리
1) 공유 메모리의 필요성
- 공유 메모리: 데이터 자체를 공유하기 위한 기능
- 프로세스는 지역 변수, 동적 변수, 전역 변수와 같은 데이터를 저장하기 위한 자신만의 메모리 영역을 가짐
- 이 메모리 영역은 다른 프로세스가 접근해서 함부로 데이터를 읽거나 쓰지 못하도록 커널에 의해 보호됨
- 만약 다른 프로세스의 메모리 영역을 침범하려고 하면 커널은 침범 프로세스에 SIGSEGV을 보내게 됨
* SEGSEGV: 할당된 메모리의 범위를 벗어나는 곳을 접근할 때 발생하는 세그멘테이션 위반 시그널
- 다수의 프로세스가 동시에 작동하는 리눅스의 특성상 프로세스의 메모리 영역은 반드시 보호되어야 하나 프로세스 사이에 메모리 영역에 있는 데이터를 공유하기 위해서는 다른 프로세스도 사용할 수 있도록 해야함
- 공유 메모리는 프로세스 사이에 메모리 영역을 공유해서 사용할 수 있도록 해줌
- 프로세스가 공유 메모리 할당을 커널에 요청하면 커널은 해당 프로세스에 메모리 공간을 할당해 주고 이후 어떤 프로세스건 해당 메모리 영역에 접근할 수 있음
2) 공유 메모리 관련 함수
- 공유 메모리를 사용하기 위해서는 먼저 공유 메모리를 생성하고 생성된 공유 메모리와 연결해야 함
- 연결된 공유 메모리를 사용하고 사용을 마친 후에는 공유 메모리와의 연결을 해제해야 함
- 공유 메모리 생성 shmget()
#include<sys/shm.h>
#include<sys/ipc.h>
int shmget(key_t key, size_t size, int shmflg);
// key를 사용하여 size 크기의 공유 메모리를 생성하고 생성된 공유 메모리의 ID를 반환
- 키(key)가 가리키는 새로운 공유 메모리를 생성하고 생성된 공유 메모리를 식별하는 식별자 ID를 반환하는 것
- 매개변수 값에 따라 새로운 공유 메모리를 생성하거나 기존의 공유 메모리를 찾아서 줌
- key
- IPC_PRIVATE: 항상 새로운 공유 메모리를 생성하고자 할 때
- ftok(char *path, int id)로 생성한 키를 사용
- shmflg
- IPC_CREAT: 새로운 공유 메모리를 생성함. 생성할 공유 메모리의 접근권한을 OR(|)하여 함께 지정
- IPC_EXCL: IPC_CREAT과 함께 사용될 때, 해당 공유 메모리가 이미 존재하면 실패함
- 이 플래그가 0이면 지정한 key와 연관된 기존의 공유 메모리를 찾아서 줌
shmid = shmget(key, 1024, IPC_CREAT|0644);
// key가 가리키는 1024 바이트의 공유 메모리를 생성하면 생성된 공유 메모리의 ID가 반환됨
- 공유 메모리 연결 shmat()
#include<sys/shm.h>
#include<sys/ipc.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
// shmid 공유 메모리를 이 프로세스의 메모리 위치 shmaddr에 연결하고 그 주소를 반환
- 생성된 공유 메모리(shmid)를 사용하기 위해서는 이 프로세스(이 시스템 호출을 실행하는 프로세스)의 특정 메모리의 위치(shmaddr)에 연결해야 함
- shmaddr가 NULL인 경우에는 커널에서 적절한 주소를 선정하여 반환
- shmflg: 공유메모리에 대한 읽기/쓰기 권한을 지정
- 시스템 호출이 정상적으로 동작한다면 적절한 포인터를 반환하고 실패하면 (void*) -1을 반환함 → void*를 반환하는 이유는 공유 메모리에 있는 데이터가 어떤 자료형인지 모르기 때문에 무엇이든 받을 수 있는 void* 자료형으로 반환
- 다음과 같이 생성된 공유메모리를 연결하면 커널에서 적절한 주소에 연결하여 그 주소를 반환해줌
shmaddr = (char *)shmat(shmid, NULL, 0);
- 공유 메모리 연결 해제 shmdt()
- shmat()에서 받은 공유 메모리에 대한 연결 포인터를 전달받아 공유 메모리에 대한 연결을 해제
- 공유 메모리 자체를 제거하는 것은 아님
#include<sys/shm.h>
#include<sys/ipc.h>
void shmdt(const void *shmaddr);
// 공유 메모리에 대한 연결 주소 shmaddr를 연결해제 함. 성공시 0 실패시 -1을 반환
- 공유 메모리 제어 shmctl()
- 공유 메모리를 제어하기 위해 사용
- 공유메모리의 정보를 얻거나 공유 메모리를 삭제하는 등의 조작을 함
#include<sys/shm.h>
#include<sys/ipc.h>
void shmctl(int shmid, int cmd, struct dhmid_ds *buf);
// shmid 공유메모리를 cmd 명령어에 따라 제어
- cmd는 shmid가 나타내는 공유 메모리를 제어하기 위한 명령어로 다음과 같은 명령어를 사용할 수 있음
- IPC_RMID: 공유 메모리를 제거
- IPC_SET: 공유 메모리의 정보(사용자 ID, 그룹 ID, 접근 권한 등)를 buf에서 지정한 값으로 바꿈
- IPC_STAT: 현재 공유 메모리의 정보를 buf에 저장
- SHM_LOCK: 공유 메모리를 잠금
- SHM_UNLOCK: 공유 메모리 잠금을 해제
- buf는 공유 메모리에 관한 정보를 저장할 수 있는 다음과 같은 shmid_ds 구조체에 대한 포인터
struct shmid_ds{
struct ipc_perm shm_perm; // 접근 권한
size_t shm_segsz; // 이 공유 메모리의 크기(바이트)
time_t shm_atime; // 최근 연결 시간
time_t shm_dtime; // 최근 연결 해제 시간
time_t shm_ctime; // 최근 변경 시간
pid_t shm_cpid; // 이 공유 메모리의 생성 프로세스 PID
pid_t shm_lpid; // 최근 연결(헤제)한 프로세스 PID
shmatt_t shm_nattch; // 공유 메모리에 대한 현재 연결 개수
...
}
3) 공유 메모리 예제
- 공유 메모리를 생성하고 이를 사용하여 다른 프로그램(프로세스)에게 메시지를 보내는 예제 프로그램
- 프로그램 shm1.c
- 공유 메모리에 사용될 키를 생성
- 해당 키가 가리키는 새로운 공유 메모리를 생성하고 이 공유 메모리의 ID를 출력
- 이 공유 메모리에 연결하고 연결된 주소에 메시지를 복사하여 공유 메모리에 보냄
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
int main(){
int shmid;
key_t key;
char *shmaddr;
key = ftok("helloshm",1);
shmid = shmget(key, 1024, IPC_CREAT|0644);
if(shmid ==-1){
perror("shmget");
exit(1);
}
printf("shmid: %d\n", shmid);
shmaddr = (char *)shmat(shmid, NULL, 0);
strcpy(shmaddr, "hello shared memory");
return(0);
}
- icps -m 명령어를 사용하여 이 프로그램에서 만든 공유 메모리를 직접 확인해볼 수 있음
- 프로그램 shm2.c
- 공유 메모리를 통해서 다른 프로그램(프로세스)으로부터 메시지를 받는 예제 프로그램
- 공유 메모리에 사용될 키를 생성
- 이 키가 가리키는 기존 공유 메모리를 획득하고 이 공유 메모리의 ID를 출력(shm1과 같은 공유 메모리 ID를 출력)
- 이 공유 메모리에 연결하고 연결된 주소를 사용하여 공유 메모리에 있는 메시지를 출력
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
int main(){
int shmid;
key_t key;
char *shmaddr;
key = ftok("helloshm",1);
shmid = shmget(key, 1024, IPC_CREAT|0644);
if(shmid ==-1){
perror("shmget");
exit(1);
}
printf("shmid: %d\n", shmid);
shmaddr = (char *)shmat(shmid, NULL, 0);
printf("%s\n", shmaddr);
return(0);
}
실행 결과에서 shm1 프로그램과 같은 공유 메모리 ID를 사용하는 것과
보내온 메시지를 읽어서 출력하는 것을 확인할 수 있음
4) 부모-자식 프로세스 사이의 메모리 공유 예제
- 자식 프로세스에서 부모 프로세스에 메시지를 보내는 예제 프로그램
- 새로운 공유 메모리를 생성
- 자식 프로세스를 생성
- 자식 프로세스: 공유 메모리에 연결하고 연결된 주소를 통해 메모리에 데이터를 보냄. 공유 메모리에 대한 연결을 해제
- 자식 프로세스가 끝나기를 기다리고 이 공유 메모리에 연결하여 공유 메모리에 있는 데이터를 출력. 이후 공유 메모리 연결을 해제하고 32번째 줄에서 공유 메모리를 제거
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
int main(){
int shmid;
char *shmptr1, *shmptr2;
shmid = shmget(IPC_PRIVATE, 10*sizeof(char),IPC_CREAT|0666);
if(shmid ==-1){
perror("shmget faild\n");
exit(0);
}
if(fork()==0){
shmptr1 = (char *)shmat(shmid, NULL, 0);
for(int i=0;i<10;i++)
shmptr1[i] = i*10;
shmdt(shmptr1);
exit(0);
}
else{
wait(NULL);
shmptr2 = (char *)shmat(shmid, NULL, 0);
for(int i=0;i<10;i++)
printf("%d ",shmptr2[i]);
printf("\n");
shmdt(shmptr2);
if(shmctl(shmid,IPC_RMID,NULL)==-1)
printf("shmctl failed\n");
}
return 0;
}
실행 결과에서 자식 프로세스가 공유 메모리를 통해 보낸 값을 부모 프로세스가 사용하는 것을 확인할 수 있음
10.5 메모리 관련 함수
1) 메모리 관련 함수
#include<string.h>
void *memset(void *s, int c, size_t n);
// s에서 시작하여 n바이트만큼 문자c로 설정한 다음에 s를 반환
int memcmp(const void *s1, const void *s2, size_t n);
// s1과 s2에서 첫 n바이트를 비교해서
// 메모리 블록 내용이 동일하면 0을 반환하고
// s1이 s2보다 작으면 음수를 반환하고
// s1이 s2보다 크다면 양수를 반환
void *memchr(const void *s, in c, size_t n);
// s가 가리키는 메모리의 n 바이트 범위에서 문자 c를 탐색함
// c와 일치하는 첫 바이트에 대한 포인터를 반환하거나, c를 찾지 못하면 NULL을 반환
void *memmove(void *dst, const void *src, size_t n);
// src에서 dst로 n바이트 복사하고, dst를 반환
void *memcpy(void *dst, const void *src, size_t n);
// src에서 dst로 n바이트를 복사함
// 두 메모리 영역은 겹쳐지지 않음
// 만일 메모리 영역을 겹쳐서 쓰길 원한다면 memmove() 함수를 사용하면 됨
// dst를 반환
2) 메모리 맵핑 함수
- 메모리 맵핑: 파일의 일부 영역에 메모리 주소를 부여할 수 있음
- 변수를 사용하는 것처럼 파일의 데이터를 읽거나 쓸 수 있음
- 메모리 주소를 나타내는 포인터와 배열을 사용하여 파일의 데이터를 다룰 수 있음
- mmap() 시스템 호출
- 열린 파일의 일부 영역(그림 10.15에서 off부터 len 크기)에 메모리 주소를 부여하고 메모리 맵핑된 영역의 시작 주소(addr)를 반환
#include<sys/types.h>
#include<sys/mman.h>
caddr_t mmap(caddr_t addr, size_t len, int prot, int flag, int fd, off_t off);
// fd가 나타내는 파일의 일부 영역(off부터 len크기)에
// 메모리 주소를 부여하고 메모리 맵핑된 영역의 시작 주소(addr)를 반환
- 매개 변수
- addr: 메모리 맵핑에 부여할 메모리 시작 주소로 이 값이 NULL이면 시스템이 적당한 시작 주소를 선택
- len: 맵핑할 파일 영역의 크기로 메모리 맵핑의 크기와 같음
- prot: 맵핑된 메모리 영역에 대한 보호 정책을 나타냄 → PROT_READ(읽기), PROT_WRITE(쓰기), PROT_EXEC(실행), PROT_NONE(접근 불가)
- fd: 대상 파일의 파일 디스크립터
- off: 맵핑할 파일 영역의 시작 위치
- flag: 메모리 맵핑의 특성을 나타냄
- MAP_FIXED: 반환 주소가 addr와 같아야 함
- MAP_FIXED가 지정되지 않고 addr가 0이 아니면, 커널은 addr를 단지 힌트로 사용함
- MAP_SHARED나 MAP_PRIVATE 중의 하나가 반드시 지정되어야 함
- MAP_SHARED: 부여된 주소에 쓰면 파일에 저장됨
- MAP_PRIVATE: 부여된 주소에 쓰면 파일의 복사본이 만들어지고 이후부터는 복사본을 읽고 쓰게 됨
- munmap() 시스템 호출
- 맵핑한 메모리 영역을 해제]
#include<sys/mman.h>
int munmap(void *addr, size_t len);
// addr에서 시작하는 len 크기인 맵핑한 메모리 영역을 해제
3) 메모리 맵핑 예제
- 메모리 맵핑을 사용하여 파일 내용을 출력하는 cat 명령어 구현
- 파일로부터 데이터를 읽는 대신에 메모리 맵핑된 주소를 배열처럼 사용해서 데이터에 접근
- 명령줄 인수로 받은 파일을 open()
- 이 파일의 크기를 알기 위해 해당 파일에 대해 fstat() 호출을 하여 파일 정보를 받음
- 이 파일 전체에 대해 메모리 맵핑을 함
- 매핑한 주소(포인터)를 사용하여 파일 내용을 한 글자씩 읽어서 출력
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/mman.h>
int main(int argc, char *argv[]){
struct stat sbuf;
char *p;
int fd;
if(argc<2){
fprintf(stderr, "사용법: %s 파일이름\n", argv[0]);
exit(1);
}
fd = open(argv[1],O_RDONLY);
if(fd==-1){
perror("open");
exit(1);
}
if(fstat(fd, &sbuf)==-1){
perror("fstat");
exit(1);
}
p = mmap(0, sbuf.st_size, PROT_READ, MAP_SHARED, fd, 0);
if(p==MAP_FAILED){
perror("mmap");
exit(1);
}
for(long l=0;l<sbuf.st_size;l++)
putchar(p[l]);
close(fd);
munmap(p, sbuf.st_size);
return 0;
}
'Study > Linux' 카테고리의 다른 글
[LinuxProgramming] Chapter 09. 프로세스 제어 (0) | 2022.06.06 |
---|---|
[LinuxProgramming] Chapter 08. 프로세스 (0) | 2022.06.05 |
[LinuxProgramming] Chapter 07. 파일 및 레코드 잠금 (0) | 2022.04.25 |
[LinuxProgramming] Chapter 06. 파일 시스템 (0) | 2022.04.25 |
[LinuxProgramming] Chapter 05. 파일 입출력 (0) | 2022.04.25 |