프로세스 간 통신 (IPC)
1. 파이프 (Pipe)
- 스트림 지향: 데이터를 순차적으로 단방향으로 전송하는 방식입니다.
- 시스템 호출: pipe(), read(), write()를 사용하여 데이터 전송을 처리합니다.
- 동작 방식: 파이프의 쓰기 끝에 데이터를 작성하면, 커널이 해당 데이터를 버퍼에 저장하고 읽기 끝에서 읽힐 때까지 유지합니다.
- FIFO 동작: First In First Out 방식으로 데이터를 처리합니다.
2. 메시지 큐 (Message Queue)
- 메시지 지향: 프로세스가 메시지를 쓰고 다른 프로세스가 읽는 형태입니다.
- 특징: 메시지 큐는 메시지 식별자로 관리되며, 커널 내에서 메시지의 연결 리스트로 구현됩니다.
- 메시지 순서: 메시지는 FIFO 방식으로 읽히지만, 비순차적으로도 메시지를 검색할 수 있습니다.
3. 공유 메모리 (Shared Memory)
- 가상 메모리 영역: 여러 프로세스가 공유하는 메모리 영역입니다.
- 정보 공유: 프로세스가 메모리의 특정 영역을 공유하여 정보를 전달합니다.
- 동기화 필요: 세마포어를 사용하여 공유 메모리에 대한 접근을 동기화해야 합니다.
4. 세마포어 (Semaphore)
- 동기화 기법: unsigned 전역 정수 변수로, 프로세스 간의 동기화를 도와줍니다.
- P와 V 연산:
- P(s): s가 0이 아니면 1 감소하고 즉시 반환합니다. 이 작업은 원자적으로 수행됩니다.
- V(s): s를 1 증가시킵니다. 이 작업도 원자적으로 수행됩니다.
- SysV와 POSIX: 각각 semget(), semctl(), semop() 및 sem_open(), sem_wait(), sem_post() 등의 시스템 호출을 사용합니다.
5. 소켓 (Sockets)
- 통신 끝점: 컴퓨터 네트워크를 통한 IPC 흐름의 끝점입니다.
- TCP/IP 지원: 다양한 네트워크 프로토콜을 사용하여 통신합니다.
POSIX (Portable Operating System Interface)
- 표준화: IEEE에서 운영 체제 간의 호환성을 유지하기 위해 제정된 표준입니다.
- 지원하는 IPC:
- Pipe: POSIX.1에서 지원
- Message Queue: POSIX.1b에서 지원
- Shared Memory: POSIX.1b에서 지원
- Semaphore: POSIX.1b에서 지원
파이프 (Pipes) 및 명령어 연결
- 명령어 연결: cmd1 | cmd2
- 파이프 |는 첫 번째 명령어(cmd1)의 출력을 두 번째 명령어(cmd2)의 입력으로 전달합니다.
- 입출력 흐름:
- cmd1의 표준 입력은 터미널 키보드에서 들어옵니다.
- cmd1의 표준 출력은 cmd2의 표준 입력으로 전달됩니다.
- cmd2의 표준 출력은 터미널 화면에 연결되어 표시됩니다.
로그 파일 처리 예시
다음은 로그 파일을 다루는 여러 명령어 예시입니다:
- 로그 파일 페이지 나누기 more:
- /var/log/auth.log와 /var/log/syslog의 내용을 합쳐서 페이지 단위로 보여줍니다.
- cat /var/log/auth.log /var/log/syslog | more
- 특정 사용자 관련 로그 검색:
- 두 로그 파일에서 'user konkuk'과 관련된 로그 라인을 출력합니다.
- cat /var/log/auth.log /var/log/syslog | grep 'user konkuk'
- 정렬된 로그 출력:
- 특정 사용자의 로그를 정렬하여 출력합니다.
- cat /var/log/auth.log /var/log/syslog | grep 'user konkuk' | sort
- 결과를 파일에 저장:
- 특정 사용자의 로그를 정렬한 후, 결과를 test라는 파일에 저장합니다.
- cat /var/log/auth.log /var/log/syslog | grep 'user konkuk' | sort > test
이와 같이 파이프를 활용하면 명령어 간에 출력을 효과적으로 연결하여 데이터를 처리할 수 있습니다.
pipe 코드 예시
int pipe(int fd[2])
fd[0] = read
fd[1] = write
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h> // for strcpy
int main(){
int pipefd[2];
char buf[1];
// fd[0] = read
// fd[1] = write
//파이프 만들고 예외처리
if (pipe(pipefd) == -1)
{
perror("pipe");
exit(EXIT_FAILURE);
}
// fork() 하고 예외 처리
pid_t childpid = fork();
if (childpid == -1)
{
perror("fork");
exit(EXIT_FAILURE);
}
// 부모가 자식에게 파이프로 메세지 보냄
if (childpid == 0)
{
close(pipefd[1]);
while (read(pipefd[0], &buf, 1) > 0)
{
write(STDOUT_FILENO, "\n", 1);
}
close(pipefd[0]);
exit(EXIT_SUCCESS);
} else {
// 부모는 write만 할것이므로 read 닫음
close(pipefd[0]);
char str[] = "1234567890";
write(pipefd[1], str, strlen(str));
close(pipefd[1]);
wait(NULL);
exit(EXIT_SUCCESS);
}
}
Message Queue
Message Oriented
MQ는 데이터가 연속적인 스트림이 아니라, 메시지 단위로 처리됩니다. 각 메시지는 독립된 단위로 송수신되며, 특정 메시지를 선택적으로 읽거나 처리할 수 있습니다.
프로세스 간 통신 (IPC)
여러 프로세스가 메시지 큐에 메시지를 쓰거나 읽을 수 있습니다. 하나의 프로세스가 작성한 메시지는 다른 프로세스에서 읽을 수 있는 형태로 구현되어 있습니다.
식별자 사용
각 메시지 큐는 고유한 식별자(Message Queue Identifier)를 통해 관리됩니다.
커널에서의 구현
메시지 큐는 커널 내부에서 연결 리스트(linked list) 형태로 구현되어 있으며, 큐에 저장된 메시지는 커널 메모리에 위치합니다. 사용자 공간에서 직접 접근할 수 없습니다. 메시지는 기본적으로 FIFO(First-In-First-Out) 방식으로 처리되며, 특정 조건에 따라 우선순위가 높은 메시지를 먼저 읽을 수도 있습니다.
활용 예시
메시지 큐는 독립적인 프로세스 간 데이터를 주고받기 위한 IPC(Inter-Process Communication) 수단으로 사용됩니다. 특히 생산자-소비자 패턴에 적합합니다.
MQ 삭제
메시지 큐는 삭제될 때까지 계속 존재합니다. 메시지 큐는 한 번 생성되면, 큐를 사용한 모든 프로세스가 종료되더라도 큐 자체는 사라지지 않습니다. 따라서 수동으로 삭제되기 전까지 시스템에 남아있습니다.
메시지 큐 삭제 방법
메시지 큐를 삭제하려면 msgctl() 시스템 콜을 사용해야 하며, IPC_RMID 명령을 통해 큐를 제거할 수 있습니다. msgctl()은 메시지 큐의 상태를 변경하거나 삭제하는 데 사용됩니다.
메시지 큐 상태 확인
ipcs 명령어를 사용하면 시스템에 존재하는 메시지 큐들을 확인할 수 있습니다. 사용하지 않는 메시지 큐가 있다면 이 명령어를 통해 목록을 확인할 수 있습니다.
메시지 큐 삭제
사용하지 않는 메시지 큐를 삭제하려면 ipcrm 명령어를 사용합니다. 이 명령어는 메시지 큐, 세마포어, 공유 메모리 등을 삭제하는 데 사용됩니다.
SysV Message Queue
Message Format
SysV 메시지 큐에서의 메시지 포맷은 msgbuf 구조체로 정의되며, 메시지에는 mtype과 mtext가 포함됩니다.
struct msgbuf{
long mtype;
char mtext[N]; // N byte
}
mtype은 필수적이며 양수여야 하며, mtext는 문자 배열로 저장할 수 있습니다.
Message Queue 생성
새로운 메시지 큐를 생성하거나 기존 큐에 접근하려면 msgget() 함수를 사용합니다.
int msgget(key_t key, int flag)
key: 시스템 전역적으로 고유한 메시지 큐의 구분자
flag: IPC_CREAT, IPC_EXCL 등 다양한 옵션이 존재
메시지 송신
메시지를 큐에 전달하려면 msgsnd() 함수를 사용합니다.
int msgsnd(int msgid, const void *ptr, size_t nbytes, int flags)
메시지 수신
메시지를 큐에서 받으려면 msgrcv() 함수를 사용합니다.
int msgrcv(int msgid, void *ptr, size_t nbytes, long type, int flag)
type: 메시지 유형을 지정
flag: 메시지가 너무 클 경우의 처리 방법
메시지 큐 삭제
메시지 큐를 삭제하려면 msgctl()을 사용합니다.
msgctl(msgid, IPC_RMID, NULL)
SysV Message Queue 예제 코드
MQ 송신자
int main() {
key_t ipckey;
int mqdes, i;
size_t buf_len;
struct {
long id;
int value;
} mymsg;
buf_len = sizeof(mymsg.value);
ipckey = ftok("./tmp/foo", 1946);
mqdes = msgget(ipckey, 0600 | IPC_CREAT);
if (mqdes == -1) {
perror("msgget");
return 1;
}
for (i = 0; i < MAX_ID; i++) {
mymsg.id = i+1;
mymsg.value = i*3;
printf("sending %ld %d\n", mymsg.id, mymsg.value);
int ret = msgsnd(mqdes, &mymsg, buf_len, 0);
if (ret == -1) {
perror("msgsnd");
return 1;
}
}
}
MQ 수신자:
int main() {
key_t ipckey;
int mqdes, i;
size_t buf_len;
struct {
long id;
int value;
} mymsg;
buf_len = sizeof(mymsg.value);
ipckey = ftok("./tmp/foo", 1946);
mqdes = msgget(ipckey, 0600 | IPC_CREAT);
if (mqdes == -1) {
perror("msgget");
return 1;
}
for (i = 0; i < MAX_ID; i++) {
int ret = msgrcv(mqdes, &mymsg, buf_len, i+1, 0);
if (ret == -1) {
perror("msgrcv");
return 1;
}
printf("received %ld %d\n", mymsg.id, mymsg.value);
}
}
지정한 id(mtype)에 맞는것을 뽑아서 온다
POSIX Message Queue
Message Queue 생성
POSIX 메시지 큐는 mq_open() 함수로 생성합니다.
mqd_t mq_open(const char *name, int oflag, mode_t mode, struct mq_attr *attr)
- name: 큐 이름
- oflag: 읽기/쓰기 모드
- mode: 큐 권한
- attr: 큐 속성
메시지 송신
메시지를 큐에 보내려면 mq_send() 함수를 사용합니다.
int mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned prio)
- msg_ptr: 메시지 내용
- msg_len: 메시지 길이
- prio: 메시지 우선순위
메시지 수신
메시지를 큐에서 받으려면 mq_receive() 함수를 사용합니다.
int mq_receive(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned *msg_prio)
- msg_ptr: 수신할 메시지 버퍼
- msg_len: 메시지의 길이
- msg_prio: 메시지 우선순위
메시지 큐 삭제
큐를 닫으려면 mq_close()를 사용하고, 큐를 시스템에서 제거하려면 mq_unlink()를 사용합니다.
mq_close(mqd_t mqdes) mq_unlink(const char *name)
POSIX Message Queue 예제 코드
MQ 송신자:
#include <mqueue.h>
#include <stdio.h>
int main() {
struct mq_attr attr;
int value = 0;
unsigned int prio;
mqd_t mqdes;
attr.mq_maxmsg = 10;
attr.mq_msgsize = MSG_SIZE;
mqdes = mq_open("/m_queue", O_CREAT | O_RDWR, 0600, &attr);
if (mqdes == -1) {
perror("mq_open");
return 1;
}
for (prio = 0; prio < MAX_PRIO; prio++) {
printf("sending value %d with priority %u\n", value, prio);
int ret = mq_send(mqdes, (char *)&value, MSG_SIZE, prio);
if (ret == -1) {
perror("mq_send");
return 1;
}
else {
value+=3;
}
}
mq_close(mqdes);
return 0;
}
MQ 수신자:
#include <stdio.h>
#include <mqueue.h>
int main() {
struct mq_attr attr;
int value;
unsigned int prio;
mqd_t mqdes;
attr.mq_maxmsg = 10;
attr.mq_msgsize = MSG_SIZE;
mqdes = mq_open("/m_queue", O_RDONLY|O_CREAT, 0600, &attr);
if (mqdes == -1) {
perror("mq_open");
return 1;
}
for (int i = 0; i < MAX_PRIO; i++) {
int ret = mq_receive(mqdes, (char *)&value, MSG_SIZE, &prio);
if (ret == -1) {
perror("mq_receive");
return 1;
}
printf("received value %d with priority %u\n", value, prio);
}
mq_close(mqdes);
mq_unlink("/m_queue");
return 0;
}
우선순위 순서대로 받아온다.
공유메모리 코드 예시
공유 메모리(Shared Memory)는 여러 프로세스가 같은 물리적 메모리 공간을 직접 접근할 수 있게 해주는 방법으로, 빠른 데이터 공유와 통신을 가능하게 합니다. 아래는 C언어를 사용한 공유 메모리 예시입니다.
1. 공유 메모리 생성 및 사용 예시 (POSIX)
Sender (공유 메모리 데이터를 작성하는 프로세스)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/stat.h>
#define SHM_NAME "/my_shared_memory" // 공유 메모리 이름
#define SIZE 4096 // 공유 메모리 크기
int main() {
// 공유 메모리 객체 생성
int shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
if (shm_fd == -1) {
perror("shm_open");
exit(1);
}
// 공유 메모리 크기 설정
if (ftruncate(shm_fd, SIZE) == -1) {
perror("ftruncate");
exit(1);
}
// 공유 메모리 맵핑
void *shm_ptr = mmap(0, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
if (shm_ptr == MAP_FAILED) {
perror("mmap");
exit(1);
}
// 데이터를 공유 메모리에 쓰기
int *data = (int *)shm_ptr;
for (int i = 0; i < 10; i++) {
data[i] = i * 2;
printf("Writing value: %d\n", data[i]);
}
// 공유 메모리 파일 디스크립터 닫기
close(shm_fd);
return 0;
}
Receiver (공유 메모리에서 데이터를 읽는 프로세스)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/stat.h>
#define SHM_NAME "/my_shared_memory" // 공유 메모리 이름
#define SIZE 4096 // 공유 메모리 크기
int main() {
// 공유 메모리 객체 열기
int shm_fd = shm_open(SHM_NAME, O_RDONLY, 0666);
if (shm_fd == -1) {
perror("shm_open");
exit(1);
}
// 공유 메모리 맵핑
void *shm_ptr = mmap(0, SIZE, PROT_READ, MAP_SHARED, shm_fd, 0);
if (shm_ptr == MAP_FAILED) {
perror("mmap");
exit(1);
}
// 데이터를 공유 메모리에서 읽기
int *data = (int *)shm_ptr;
for (int i = 0; i < 10; i++) {
printf("Reading value: %d\n", data[i]);
}
// 공유 메모리 파일 디스크립터 닫기
close(shm_fd);
// 공유 메모리 unlink
shm_unlink(SHM_NAME);
return 0;
}
1. 공유 메모리 생성 (Sender):
- shm_open()을 사용하여 공유 메모리 객체를 생성하고, ftruncate()로 메모리 크기를 지정합니다.
- mmap()을 사용하여 해당 메모리를 프로세스의 주소 공간에 맵핑합니다.
- 데이터를 shm_ptr을 통해 공유 메모리에 작성합니다.
2. 공유 메모리 읽기 (Receiver):
- shm_open()으로 이미 생성된 공유 메모리 객체를 엽니다.
- mmap()으로 공유 메모리를 프로세스의 주소 공간에 맵핑합니다.
- 데이터를 읽어서 출력합니다.
공유 메모리 종료 및 삭제
사용이 끝난 후 shm_unlink()를 사용하여 공유 메모리 객체를 삭제합니다.
세마포어 코드 예시
1. 이진 세마포어 (Binary Semaphore): 0과 1의 값만을 가질 수 있는 세마포어.
2. 카운팅 세마포어 (Counting Semaphore): 여러 개의 자원을 관리할 수 있는 세마포어로, 특정 자원의 개수를 세는 방식으로 동작합니다.
Sender (세마포어를 사용하여 메시지를 보내는 프로세스)
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
sem_t semaphore; // 세마포어 선언
void* send_message(void* arg) {
sem_wait(&semaphore); // 세마포어 획득 (세마포어 값이 0이면 대기)
printf("Sending message...\n");
sleep(1); // 메시지 전송 시뮬레이션
printf("Message sent!\n");
sem_post(&semaphore); // 세마포어 반납 (세마포어 값 증가)
return NULL;
}
int main() {
pthread_t sender1, sender2;
// 세마포어 초기화 (초기값 1)
if (sem_init(&semaphore, 0, 1) != 0) {
perror("sem_init");
exit(1);
}
// 두 프로세스가 동시에 메시지를 보내려고 함
pthread_create(&sender1, NULL, send_message, NULL);
pthread_create(&sender2, NULL, send_message, NULL);
// 스레드 종료 대기
pthread_join(sender1, NULL);
pthread_join(sender2, NULL);
// 세마포어 해제
sem_destroy(&semaphore);
return 0;
}
Receiver (세마포어를 사용하여 메시지를 수신하는 프로세스)
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
sem_t semaphore; // 세마포어 선언
void* receive_message(void* arg) {
sem_wait(&semaphore); // 세마포어 획득 (세마포어 값이 0이면 대기)
printf("Receiving message...\n");
sleep(1); // 메시지 수신 시뮬레이션
printf("Message received!\n");
sem_post(&semaphore); // 세마포어 반납 (세마포어 값 증가)
return NULL;
}
int main() {
pthread_t receiver1, receiver2;
// 세마포어 초기화 (초기값 1)
if (sem_init(&semaphore, 0, 1) != 0) {
perror("sem_init");
exit(1);
}
// 두 프로세스가 동시에 메시지를 수신하려고 함
pthread_create(&receiver1, NULL, receive_message, NULL);
pthread_create(&receiver2, NULL, receive_message, NULL);
// 스레드 종료 대기
pthread_join(receiver1, NULL);
pthread_join(receiver2, NULL);
// 세마포어 해제
sem_destroy(&semaphore);
return 0;
}
설명
- Sender와 Receiver 모두 세마포어를 사용하여 자원을 요청하고 반납하는 방식으로 동기화가 이루어집니다.
- send_message와 receive_message는 메시지를 처리하는 함수로, 세마포어의 상태를 제어하여 동기화를 맞추고 있습니다.
- 이 예시에서는 메시지 송신과 수신이 단순화되어 있지만, 실제 메시지 송수신의 구현에서는 세마포어가 자원에 대한 접근을 제어하는 데 활용됩니다.
세마포어 자체는 자원 접근의 동기화만 담당하며, 메시지를 전달하는 데 필요한 구체적인 내용은 다른 메커니즘(예: 메시지 큐, 공유 메모리 등)을 통해 처리해야 합니다.
예시 명령어
- cat: 파일을 연결하여 표준 출력에 출력합니다.
- 예: cat /etc/hostname → 시스템의 호스트 이름 출력
- more: 텍스트를 한 화면씩 페이지 나누어 보여줍니다.
- 예: more /etc/hostname
- grep: 특정 패턴이 포함된 줄을 출력합니다.
- 예: grep 'authentication failure' /var/log/auth.log
- sort: 텍스트 파일의 줄을 정렬합니다.
- 예: sort /etc/passwd
로그 파일 연결 및 검색
- 특정 사용자에 대한 로그 라인을 찾아 정렬 후 파일에 저장하는 명령어 예시:bash
-
코드 복사cat /var/log/auth.log /var/log/syslog | grep 'user konkuk' | sort > test
요약
IPC는 다양한 방법을 통해 프로세스 간의 데이터를 교환하고 동기화하는 데 필수적인 요소입니다. 각 기법은 고유한 특성과 장단점을 가지고 있어, 상황에 맞는 방법을 선택하여 사용해야 합니다.
'CS 지식 > 시스템 프로그래밍' 카테고리의 다른 글
[C언어] pthread 라이브러리를 이용한 스레드 동기화 (0) | 2025.01.28 |
---|---|
[C언어] 스레드의 개념과 pthread 라이브러리 (0) | 2025.01.28 |
Assembly(어쌤블리어)의 기초 (0) | 2024.10.18 |
[C언어] Signal 프로그래밍에서의 Race Condition과 해결법 (0) | 2024.10.17 |
[C언어] Signal 프로그래밍 개념과 예제 (0) | 2024.10.17 |
댓글