-
[Linux] 공유 메모리(Shared Memory)로 채팅 프로그램 구현하기 _IPCLinux 2023. 4. 10. 18:24728x90반응형
IPC 공유 메모리(Shared Memory)로 채팅 프로그램 구현하기
이번 운영체제 과제가 리눅스 환경에서 IPC 기술을 이용한 채팅 프로그램을 만드는 거였어요.
🍊 Shared Memory와 관련된 함수
puts()
puts는 문자열을 출력하는 함수입니다. puts 함수는 인자로 받은 문자열을 화면에 출력하고, 마지막에 자동으로 개행문자(\n)를 추가해줍니다.
getpid()
getpid()는 C언어에서 사용되는 함수 중 하나로, 현재 프로세스의 ID(Process ID, PID)를 반환하는 함수입니다.
getpid() 함수는 unistd.h 라이브러리에 선언되어 있습니다.
shmget()
공유 메모리(segment)를 생성하거나, 이미 생성된 공유 메모리를 얻는데 사용됩니다.
공유 메모리는 여러 프로세스가 동시에 접근 가능한 메모리 영역입니다. shmget() 함수를 통해 생성된 공유 메모리에 데이터를 저장하면, 다른 프로세스에서도 그 데이터에 접근하여 사용할 수 있습니다.
shmget() 함수는 sys/shm.h 헤더 파일에 선언되어 있습니다.
#include <sys/shm.h> int shmget(key_t key, size_t size, int shmflg);
- key: 공유 메모리를 구분하기 위한 키(key) 값입니다. 다른 프로세스와 공유할 공유 메모리를 식별하기 위해 사용됩니다.
- size: 공유 메모리의 크기입니다.
- shmflg: 공유 메모리를 생성할 때 사용되는 플래그입니다. 예를 들어, IPC_CREAT 플래그를 사용하면 공유 메모리가 새롭게 생성됩니다.
0666
shmid = shmget(key, sizeof(struct memory), IPC_CREAT | 0666);
**0666**은 C 언어에서 파일 권한(permission)을 지정하는 값 중 하나입니다. 이 값은 8진수(octal)로 표현됩니다.
0666은 각 비트(bit)가 아래와 같이 설정되어 있는 권한을 나타냅니다.
- 0 : 해당 비트는 권한 설정에 사용되지 않습니다.
- 6 : 사용자(user)에 대한 권한을 나타냅니다. 6은 이진수로 110인데, 이는 읽기(read)와 쓰기(write) 권한이 모두 허용됨을 나타냅니다.
- 6 : 그룹(group)에 대한 권한을 나타냅니다. 사용자 그룹도 읽기와 쓰기 권한이 모두 허용됩니다.
- 6 : 기타 사용자(other)에 대한 권한을 나타냅니다. 이 역시 읽기와 쓰기 권한이 모두 허용됩니다.
따라서 shmget() 함수 호출 시 IPC_CREAT 플래그와 함께 사용되는 권한 값 0666은 새로 생성되는 공유 메모리 세그먼트가 모든 사용자에게 읽기와 쓰기 권한이 모두 허용된 상태로 생성된다는 것을 의미합니다.
그러나 실제로는 보안상의 이유로 이러한 모든 권한을 부여하는 것은 위험할 수 있습니다. 따라서 실제로는 사용자, 그룹, 기타 사용자에 대해 적절한 권한만 부여하여 보안을 유지하는 것이 중요합니다.
shmat()
shmat()함수가 받는 인자
- shmid: shmget() 함수로 생성한 공유 메모리 세그먼트의 식별자
- shmaddr: 연결할 가상 주소의 힌트(hint) 값. **NULL**을 지정하면 커널이 자동으로 주소를 선택합니다.
- shmflg: 연결할 때 사용할 플래그 값. 주요 플래그로는 SHM_RDONLY (읽기 전용), SHM_RND (주소 랜덤화) 등이 있습니다.
shmat() 함수는 성공하면 연결된 공유 메모리 세그먼트의 시작 주소를 반환하며, 실패하면 **(void *)-1**을 반환합니다.
shmat() 함수로 공유 메모리를 연결한 후에는 해당 메모리를 자유롭게 읽고 쓸 수 있습니다. 연결된 메모리를 더 이상 사용하지 않을 때는 shmdt() 함수를 호출하여 연결을 해제해야 합니다.
signal()
signal() 함수는 UNIX 계열 운영체제에서 시그널 처리를 위해 사용됩니다. 시그널(signal)은 프로세스나 스레드 등에게 발생한 이벤트를 나타내며, 예기치 않은 상황이 발생했을 때 이를 처리하는 데 사용됩니다. 예를 들어, 프로세스가 다른 프로세스에 의해 강제로 종료되는 경우 SIGKILL 시그널이 발생하며, 이를 처리하기 위해서는 signal() 함수를 사용해야 합니다.
signal() 함수는 다음과 같은 형식을 가집니다.
void (*signal(int signum, void (*handler)(int)))(int);
- signum: 처리할 시그널 번호. 예를 들어, SIGINT 시그널의 경우 signum 값은 **2**입니다.
- handler: 시그널 처리 함수의 포인터. 시그널이 발생했을 때 호출될 함수를 지정합니다.
signal() 함수는 이전에 등록된 시그널 처리 함수를 반환합니다. 만약 이전에 등록된 함수가 없다면 **SIG_DFL**을 반환합니다. 따라서, **signal(signum, SIG_IGN)**과 같이 **SIG_IGN**을 전달하여 시그널을 무시하도록 설정할 수 있습니다.
signal() 함수는 POSIX 표준에서는 권장하지 않습니다. 대신 sigaction() 함수를 사용하는 것이 좋습니다. sigaction() 함수는 signal() 함수보다 더 많은 기능을 제공하며, 이식성도 높습니다.
fgets()
fgets() 함수는 C 언어에서 문자열을 입력 받을 때 사용하는 함수입니다. stdin 파일 포인터로부터 문자열을 읽어들입니다.
다음은 fgets() 함수의 형식입니다.
char *fgets(char *str, int n, FILE *stream);
- str: 읽어들인 문자열을 저장할 버퍼의 주소를 가리키는 포인터입니다.
- n: 읽어들일 문자열의 최대 길이입니다. 이 길이를 초과하지 않는 문자열만 읽어들입니다.
- stream: 문자열을 읽어들일 파일 포인터입니다. 일반적으로 **stdin**을 전달합니다.
fgets()함수는 읽어들인 문자열을 str 버퍼에 저장하며, 문자열의 끝에는 자동으로 NULL 문자(\\0)가 추가됩니다.
kill()
kill() 함수는 UNIX 계열 운영체제에서 다른 프로세스나 프로세스 그룹에게 시그널을 보내는 함수입니다. 시그널을 보내어 프로세스나 프로세스 그룹을 제어할 수 있습니다.
kill() 함수의 형식은 다음과 같습니다.
int kill(pid_t pid, int sig);
- pid: 시그널을 보낼 프로세스나 프로세스 그룹의 ID입니다. pid 값이 양수인 경우는 프로세스의 ID를 나타내며, 음수인 경우는 프로세스 그룹의 ID를 나타냅니다. pid 값이 0인 경우는 현재 프로세스의 프로세스 그룹에 시그널을 보내며, -1인 경우는 모든 프로세스에 시그널을 보냅니다.
- sig: 보낼 시그널의 번호입니다. 시그널은 정수로 표현되며, 예를 들어 **SIGKILL**의 경우 **9**입니다.
kill() 함수를 호출하면 지정한 프로세스나 프로세스 그룹에 시그널을 보냅니다. 시그널을 보낸 후에는 시그널이 처리될 때까지 기다리지 않으며, 다음 명령어를 수행합니다. 시그널이 처리되면 시그널 처리 함수가 호출됩니다. 이 때 시그널 처리 함수가 지정되어 있지 않다면, 기본적으로 시스템이 제공하는 처리 방식으로 처리됩니다.
kill() 함수는 프로세스나 프로세스 그룹을 제어할 수 있는 중요한 함수 중 하나입니다. 예를 들어, SIGKILL 시그널을 보내어 프로세스를 강제로 종료할 수 있습니다.
shmdt()
shmdt() 함수는 shmat() 함수로 공유 메모리를 연결한 프로세스가 해당 공유 메모리와의 연결을 끊을 때 사용하는 함수입니다.
shmdt() 함수의 형식은 다음과 같습니다.
int shmdt(const void *shmaddr);
- shmaddr: 끊을 공유 메모리 영역의 주소입니다. 이 값은 shmat() 함수 호출 시 반환된 포인터와 동일해야 합니다.
shmdt() 함수를 호출하면 해당 프로세스와 공유 메모리 간의 연결이 끊어지며, 이제부터 해당 공유 메모리에 접근할 수 없게 됩니다. 공유 메모리 영역이 삭제되는 것은 아니며, 다른 프로세스에서는 여전히 접근할 수 있습니다.
shmctl()
shmctl() 함수는 공유 메모리에 대한 제어 작업을 수행하는 함수입니다. 공유 메모리 세그먼트의 제거, 상태 확인, 권한 변경 등의 작업을 할 수 있습니다.
shmctl() 함수의 형식은 다음과 같습니다.
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- shmid: 제어할 공유 메모리 세그먼트의 ID입니다. 이 값은 shmget() 함수 호출 시 반환된 ID와 동일해야 합니다.
- cmd: 수행할 명령어를 지정합니다. 여러 가지 명령어를 사용할 수 있습니다. 일부 명령어는 buf 인자를 요구하며, **buf**는 해당 명령어의 인자를 저장하는 구조체입니다. 주요한 명령어는 다음과 같습니다.
- IPC_RMID: 공유 메모리 세그먼트를 제거합니다.
- IPC_STAT: 공유 메모리 세그먼트의 상태 정보를 가져옵니다. 이 때, buf 인자에는 shmid_ds 구조체를 전달해야 합니다.
- IPC_SET: 공유 메모리 세그먼트의 권한을 변경합니다. 이 때, buf 인자에는 shmid_ds 구조체를 전달해야 하며, shmid_ds 구조체에 새로운 권한 정보를 설정합니다.
shmctl() 함수는 shmid 인자로 지정된 공유 메모리 세그먼트에 대해 cmd 인자로 지정된 명령어를 수행합니다. 명령어에 따라 buf 인자를 사용하여 추가 정보를 전달하거나, 반환 값을 확인할 수 있습니다.
공유 메모리를 사용하는 프로그램에서는 공유 메모리 세그먼트를 제거하기 전에 반드시 shmdt() 함수를 호출하여 모든 연결을 끊은 후에 shmctl() 함수를 호출하여 공유 메모리 세그먼트를 제거해야 합니다.
🍊 턴 방식 채팅 프로그램을 구현해보자
📁 clientA.c
#include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/types.h> #include <unistd.h> #define FILLED 0 #define READY 1 #define NOT_READY -1 struct memory { char buff[100]; int status, pid1, pid2; }; struct memory* shmptr; // clientB로부터 받은 메시지 출력하는 함수 void handler(int signum) { // signum과 SIGUSR1이 같으면 user1은 clientB로부터 메시지를 받는다 if (signum == SIGUSR1) { printf("\n수신 대기... \n"); printf("B) "); puts(shmptr->buff); // 수신한 문자열이 exit일 경우 if(strcmp(shmptr->buff, "exit\n") == 0) { printf("==== 프로그램을 종료합니다. :( ====\n"); exit(0); // 프로그램 종료 } } } int main() { int pid = getpid(); // 현 프로세스(user1)의 pid int shmid; int key = 12345; // shared memory의 키값 // shared memory 생성하기 shmid = shmget(key, sizeof(struct memory), IPC_CREAT | 0666); // shared memory와 프로세스 가상 주소 공간과 연결 shmptr = (struct memory*)shmat(shmid, NULL, 0); // 마지막 파라미터 0의 의미: 플래그를 사용하지 않음 // shared memory에 프로세스 id 저장, 준비되지 않은 상태(NOT_READY) 저장 shmptr->pid1 = pid; shmptr->status = NOT_READY; // handler 이용한 시그널 처리 signal(SIGUSR1, handler); while (1) { while (shmptr->status != READY) continue; sleep(1); // 메시지 입력받기 printf("입력 대기: \n"); // 버퍼에 메시지 저장, 최대길이, 문자열 읽어들일 파일 포인터 fgets(shmptr->buff, 100, stdin); shmptr->status = FILLED; // 상태 변경 // kill 함수로 clientB에게 메시지 전송 kill(shmptr->pid2, SIGUSR2); if(strcmp(shmptr->buff, "exit\n") == 0) { printf("==== 프로그램을 종료합니다. :( ====\n"); exit(0); // 프로그램 종료 break; } } // 사용 종료: 공유 메모리와 연결 해제 shmdt((void*)shmptr); // 공유 메모리 세그먼트 제거 shmctl(shmid, IPC_RMID, NULL); return 0; }
📁 clienB.c
#include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/types.h> #include <unistd.h> #define FILLED 0 #define READY 1 #define NOT_READY -1 // 메모리 구조체 struct memory { char buff[100]; int status, pid1, pid2; }; // 공유 메모리를 저장할 memory 구조체의 전역변수 생성 struct memory* shmptr; void handler(int signum) { // clientA로부터 메시지 수신 if (signum == SIGUSR2) { printf("\n수신 대기... \n"); printf("A) "); puts(shmptr->buff); // 수신받은 메시지가 exit일 경우 if(strcmp(shmptr->buff, "exit\n") == 0) { printf("==== 프로그램을 종료합니다. :( ====\n"); exit(0); // 프로그램 종료 } } } int main() { int pid = getpid(); int shmid; int key = 12345; // shared memory 생성하기 shmid = shmget(key, sizeof(struct memory), IPC_CREAT | 0666); // shared memory와 프로세스 가상 주소 공간과 연결 shmptr = (struct memory*)shmat(shmid, NULL, 0); // shared memory에 현 프로세스(clientB)의 id 저장, 준비되지 않은 상태(NOT_READY) 저장 shmptr->pid2 = pid; shmptr->status = NOT_READY; signal(SIGUSR2, handler); while (1) { sleep(1); // 메시지 입력받기 printf("입력 대기: \n"); fgets(shmptr->buff, 100, stdin); shmptr->status = READY; kill(shmptr->pid1, SIGUSR1); // 입력받은 메시지가 eixt일 경우 if(strcmp(shmptr->buff, "exit\n") == 0) { printf("==== 프로그램을 종료합니다. :( ====\n"); exit(0); // 프로그램 종료 break; } while (shmptr->status == READY) continue; } // 사용 종료: 공유 메모리와 연결 해제 shmdt((void*)shmptr); return 0; }
🍊 후기
아 지인짜 오랜만에 C를 했어요. 다 까먹었을 줄 알았는데 그래도 하다보니 기억이 나더라구요. 자료구조 공부를 열심히 해둬서 참 다행이에요.
LIST'Linux' 카테고리의 다른 글
[Linux] 멀티 스레드와 싱글 스레드 성능 비교하기 _pthread (0) 2023.04.14