프로세스 간 통신은 두 프로세스 간의 통신을 말합니다. 시스템의 각 프로세스는 자체 주소 공간을 가지고 있으며 서로 독립적이고 격리되어 있으며, 각 프로세스는 자체 주소 공간에 있습니다. 따라서 같은 프로세스의 다른 모듈 간의 통신은 전역 변수를 사용하는 등 매우 간단합니다.
서로 다른 주소 공간에 있기 때문에 서로 다른 두 프로세스 간에 통신하기 어려운 경우가 많지만 Linux 커널은 프로세스 간 통신을 위한 여러 가지 메커니즘을 제공합니다.
프로세스 간 통신 메커니즘
Linux 커널은 주로 UNIX 시스템에서 상속된 다양한 IPC 메커니즘을 제공합니다.
- UNIX IPC파이프라인, FIFO, 신호
- 시스템 V IPC(로컬화된 프로세스 간 통신): 세마포어, 메시지 큐, 공유 메모리
- POSIX IPC(네트워크의 프로세스 간 통신): 세마포어, 메시지 큐, 공유 메모리, 소켓
프로세스 통신 구현 원칙: 통신 중인 프로세스는 서로 동일한 메모리에 액세스할 수 있습니다.
파이프라인
한 프로세스와 다른 프로세스를 연결하는 데이터의 흐름을 파이프라고 합니다.
파이프라인의 핵심은 파일입니다. 한 프로세스는 파일을 쓰기로 열고, 다른 프로세스는 읽기로 열고, 첫 번째 프로세스는 쓰고 두 번째 프로세스는 읽음으로써 통신이 이루어집니다.
익명 파이프
친족 관계에 있는 프로세스 간의 단방향 통신을 위한 특별한 종류의 파이프
익명 파이프 만들기
#include<unistd.h>
int pipe(int fd[2]);
/*
* fd배열: 파이프의 양쪽 끝에 있는 두 개의 파일 식별자.,fd[0]파일 디스크립터 fd 읽기[1]파일 기술자 작성하기
* flags: 파이프라인 플래그는 0 또는 다른 매개변수일 수 있습니다.
* -O_NONBLOCK:비차단 모드, 즉 프로세스를 차단하지 않고 파이프를 읽고 쓰는 모드를 설정합니다.
* -O_CLOEXEC:실행 시스템 호출이 실행될 때 파이프라인이 닫히도록 설정합니다.
* 반환 값: 성공 시 0, 실패 시 -1, errno를 설정합니다.
*/
부모 및 자식 프로세스는 익명 파이프를 통해 통신합니다.
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char const *argv[])
{
int fd[2] = {0};
int ret = pipe(fd);
if (ret == -1)
{
perror("pipe");
exit(-1);
}
ret = fork();
if (ret == 0) // 자식 프로세스는 데이터를 쓰고 읽기 측을 닫습니다.
{
close(fd[0]);
int count = 5;
const char *str = "Hello father, i am child.";
while (count--)
{
write(fd[1], str, strlen(str));
sleep(1);
}
}
else if (ret > 0) // 부모는 데이터를 읽고 쓰기 쪽을 닫습니다.
{
close(fd[1]);
char buf = {0};
while (1)
{
ssize_t size = read(fd[0], buf, 1024);
if (size > 0)
printf("buf:%s
", buf);
else
break;
}
}
else
{
perror("fork");
exit(-1);
}
return 0;
}
리눅스 명령 표기법은 실제로 이전 명령의 출력을 가져와서 이후 명령의 입력으로 파이프하여 통신을 완료하는 익명 파이프입니다.
파이프라인 기능
1. 단방향 커뮤니케이션
파이프는 데이터가 한 방향으로만 흐르도록 하는 일방통행로와 같습니다. 두 프로세스가 서로 통신하려면 파이프라인 1: 부모 쓰기, 자식 읽기, 파이프라인 2: 자식 쓰기, 부모 읽기 등 두 개의 파이프라인을 만들어야 합니다.
2. 파이프라인의 본질은 문서입니다.
한 프로세스는 파일을 쓰기로 열고, 다른 프로세스는 읽기로 열고, 첫 번째 프로세스는 쓰고 두 번째 프로세스는 읽음으로써 통신이 이루어집니다.
3. "혈연" 관계의 과정
파이프는 파이프를 열고 파이프의 이름을 알지 못합니다. 이러한 종류의 파이프를 익명 파이프라고 하며, 포크를 통해 익명 파이프 정보를 상속해야 하므로 익명 파이프는 서로 관련된 IPC 프로세스에만 사용할 수 있습니다.
3. 동기화 메커니즘
읽기 끝이 파이프라인에서 데이터를 읽고 있을 때 데이터가 없으면 쓰기 끝이 데이터를 쓸 때까지 차단하고 기다리며, 읽기 끝이 데이터를 읽고 있으면 쓰기 끝이 읽기 끝을 차단하고 기다리므로 파이프라인에는 동기화 및 뮤텍스 메커니즘이 있습니다.
익명 파이워크
부모 프로세스는 자식 프로세스를 생성하기 전에 파이프라인 파일을 열고 포크하여 자식 프로세스를 생성한 다음, 동일한 파이프라인 통신 사용 목적을 달성하기 위해 부모 프로세스의 주소 공간을 복사하여 동일한 파이프라인 파일의 설명자를 가져와야 합니다.
왜 하프 듀플렉스인가요?
부모 프로세스가 포크 함수를 성공적으로 호출한 후, 부모 프로세스와 자식 프로세스는 읽기 및 쓰기 파일 설명자를 동시에 유지할 수 없으며 읽기 또는 쓰기 파일 설명자를 닫아야 합니다. 부모 프로세스와 자식 프로세스가 동시에 읽고 쓰면서 데이터 오류가 발생하는 것을 방지하기 위해 파이프가 하나만 있으므로 반이중 통신입니다. 전이중을 달성하려면 두 개의 파이프를 만들 수 있습니다.
왜 프로세스는 친밀도로만 통신할 수 있을까요?
연결되지 않은 프로세스 파일 테이블은 동일한 파일에 액세스할 수 없으며, 프로세스는 동일한 파이프 파이프라인에 액세스할 수 없으므로 이름 없는 파이프 프로세스를 통해 통신할 수 없습니다.
명명된 파이프
FIFO 파일(네임드 파이프라고도 함)은 Linux에서 프로세스 간 통신에 사용되는 특수한 유형의 파일로, FIFO 파일을 사용하면 서로 관련이 없는 프로세스가 같은 파일을 읽고 쓰면서 통신할 수 있습니다.
기능:
- FIFO 파일은 파일 시스템에 있으며 다른 파일과 마찬가지로 액세스하고 관리할 수 있습니다.
- 파일 디스크립터에만 의존하지 않고 이름으로 FIFO 파일을 인식하고 참조할 수 있습니다.
- 서로 다른 프로세스 간의 양방향 통신을 허용하는 FIFO 파일
mkfifo 명령을 사용하여 FIFO 파일 만들기
mkfifo 함수를 사용하여 FIFO 파일 만들기
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
/*
* pathname: 명명된 파이프라인 파일을 만들 때 경로+
* modePOSIX 신호는 POSIX 버전보다 훨씬 간단하고 널리 사용됩니다.
*/
독립적인 두 프로세스 간의 커뮤니케이션
아이디어 : 서버 서버와 클라이언트 클라이언트 두 개의 독립적 인 프로세스 생성, 서버 서버는 파이프 라인 파일을 여는 방법을 만들고 읽고, 클라이언트 클라이언트는 파이프 라인 파일을 여는 방법을 쓰고, 두 프로세스를 열면 클라이언트가 통신의 끝을 닫는 프로세스 통신, 통신의 끝이 될 수 있습니다.
common.h
#pragma once
const char* fifo_name = "./fifo"; //
server.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include "common.h"
int main(int argc, char const *argv[])
{
//
// 1.네임드 파이프 만들기
int ret = mkfifo(fifo_name, 0666);
if (ret < 0)
{
perror("mkfifo");
exit(-1);
}
// 2.파일을 읽기로 열기
int rfd = open(fifo_name, O_RDONLY);
if (rfd < 0)
{
perror("open");
exit(-1);
}
// 3POSIX의 신호는 POSIX의 신호보다 훨씬 간단하고 널리 사용됩니다.
while (1)
{
char buff[64];
ssize_t n = read(rfd, buff, sizeof(buff) - 1);
buff[n]='\0';
if(n>0)
printf("Server get message %s
",buff);
else if(n==0){
printf("Client closed.
");
break;
}else{
perror("read");
break;
}
}
// 4.명명된 파이프라인 파일 닫기 및 삭제하기
close(rfd);
unlink(fifo_name);
return 0;
}
client.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "common.h"
int main(int argc, char const *argv[])
{
//
// 1.파일을 쓰기로 열기
int wfd = open(fifo_name, O_WRONLY);
if (wfd < 0)
{
perror("open");
exit(-1);
}
// 2POSIX 버전의 신호는 훨씬 더 간단하고 응용 범위가 더 넓습니다.
char buff[64];
while (1)
{
printf("client send message: ");
fgets(buff, sizeof(buff) - 1, stdin);
buff[strlen(buff) - 1] = '\0'; //
'
if (strcmp(buff, "exit") == 0)
{
printf("Client closed.
");
break;
}
write(wfd, buff, strlen(buff));
}
// 3.파일 닫기
close(wfd);
return 0;
}
공유 메모리
시스템 V 표준에서 가장 성공적인 통신 방법 중 하나는 매우 빠른 속도를 특징으로 합니다.
원칙 : 물리적 메모리에서 공통 영역을 열어 두 개의 서로 다른 프로세스의 가상 주소가 동시에이 공간과의 매핑 관계를 설정하고, 이때 두 개의 독립 프로세스가 동일한 공간을 볼 수 있으며,이 공간에 직접 쓰거나 읽을 수 있으며,이 공용 영역은 공유 메모리입니다.
공유 메모리 지식
공유 메모리를 위한 데이터 구조
공유 영역은 가상 주소 공간의 버퍼 영역으로, 스택 성장 및 확장을 위한 영역으로 사용하거나 이 경우 공유 메모리와 같은 다양한 프로세스 간 공통 리소스를 저장하는 데 사용할 수 있습니다.
공유 메모리는 두 프로세스 간의 통신 이상의 용도로 사용되므로 지속성이 보장되어야 하며, 이는 프로세스가 아니라 운영 체제를 따르기 때문에 운영 체제에서 공유 메모리의 상태를 설명해야 함을 의미합니다.
공유 메모리용 shm
struct shmid_ds
{
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
구조체 ipc_perm은 공유 메모리에 대한 기본 정보를 저장합니다.
struct ipc_perm
{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode;
unsigned short seq;
};
공유 메모리 생성
shmget 함수를 사용하여
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
/*
* key: 공유 메모리를 만들 때 고유한 키 값으로, ftok 함수로 얻습니다.
* size페이지 페이지와 같은 크기인 4096 크기의 공유 메모리를 생성하여 IO 효율성을 향상시킵니다.
* shmflg비트맵을 사용하면 공유 메모리가 생성되는 방식과 공유 메모리를 생성할 수 있는 권한을 설정할 수 있습니다.
* -IPC_CREAT 공유 메모리를 생성하고 공유 메모리가 있는 경우 이를 사용합니다.
* -IPC_EXCL IPC 사용_CREAT 공유 메모리를 생성할 때 공유 메모리가 이미 존재하면 생성에 실패합니다.
* -권한 공유 메모리도 파일이므로 권한을 파일 0666의 시작 권한으로 설정할 수 있습니다.
* 반환 값: 생성에 성공하면 공유 메모리의 shmid, 실패하면 -1을 반환합니다.
*/
참고: 운영 체제에서 동일한 키로 동일한 공유 메모리를 먼저 생성/열어야만 다른 프로세스가 동일한 리소스를 볼 수 있습니다.
공유 메모리 common.hpp 만들기
#include <iostream>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
using namespace std;
#define PATHNAME "." //
#define PROJID 0x29C // 항목 번호
const int gsize = 4096;
const mode_t mode = 0666;
//10진수를 16진수로 변환하기.
string toHEX(int x)
{
char buffer[64];
snprintf(buffer, sizeof buffer, "0x%x", x);
return buffer;
}
// 키 가져오기
key_t getKey()
{
// ftok이 함수는 프로젝트의 경로에 따라 키를 계산합니다. + 항목 번호 + 특수 알고리즘
key_t key = ftok(PATHNAME, PROJID);
if (key == -1)
{
// 실패, 프로세스 종료
perror("ftok");
exit(-1);
}
return key;
}
// 공유 메모리 생성하기
int createShm(key_t key, size_t size)
{
return shmHelper(key, size, IPC_CREAT | IPC_EXCL | mode);
}
// 공유 메모리 확보하기
int getShm(key_t key, size_t size)
{
return shmHelper(key, size, IPC_CREAT);
}
int shmHelper(key_t key, size_t size, int flags)
{
int shmid = shmget(key, size, flags);
if (shmid == -1)
{
perror("shmget");
exit(-1);
}
return shmid;
}
서버 측 프로세스 server.cpp
#include <iostream>
#include "common.hpp"
using namespace std;
int main()
{
// 서버 측에서 공유 메모리 만들기
key_t key = getKey();
int shmid = createShm(key, gsize);
cout << "server key: " << toHEX(key) << endl;
cout << "server shmid: " << shmid << endl;
return 0;
}
클라이언트 프로세스 client.cpp
#include <iostream>
#include "common.hpp"
using namespace std;
int main()
{
// 클라이언트가 공유 메모리 열기
key_t key = getKey();
int shmid = getShm(key, gsize);
cout << "client key: " << toHEX(key) << endl;
cout << "client shmid: " << shmid << endl;
return 0;
}
생성된 공유 메모리는 ipcs -m으로 확인할 수 있습니다.
프로세스 연결
공유 메모리는 프로세스가 공유 메모리와 연결된 후에만 페이지 테이블을 통해 프로세스의 가상 주소 공간에 있는 공유 영역에 매핑됩니다.
상관 관계에는 shmat 함수가 필요합니다.
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
/*
* shmid연결할 공유 메모리 ID
* shmaddr프로세스 공유 영역과 연결된 공유 메모리의 주소(일반적으로 NULL).
* shmflgPOSIX 버전의 시그널은 POSIX 버전의 시그널보다 훨씬 간단하고 널리 사용됩니다.
* 반환값: 상관 관계 후 공유 영역에 매핑된 공유 메모리의 주소. 일반적으로 통신 데이터는 문자이므로 shmat의 반환값을 문자로 강제 설정할 수 있습니다.*
*/
서버 측 프로세스 server.cpp
#include <iostream>
#include "common.hpp"
using namespace std;
int main()
{
// 서버 측에서 공유 메모리 만들기
key_t key = getKey();
int shmid = createShm(key, gsize);
// 연관 공유 메모리
char *start = (char *)shmat(shmid, NULL, 0);
if ((void *)start == (void *)-1)
{
perror("shmat");
shmctl(shmid, IPC_RMID, NULL); //예외가 발생하더라도 공유 메모리는 반드시 해제되어야 합니다.
exit(-1);
}
// 연결에 성공하면 5초 동안 잠자기 상태로 있다가 해제합니다.
printf("start: %p
", start);
sleep(5);
// 공유 메모리 해제
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
클라이언트 프로세스 client.cpp
#include <iostream>
#include "common.hpp"
using namespace std;
int main()
{
// 클라이언트가 공유 메모리 열기
key_t key = getKey();
int shmid = getShm(key, gsize);
char *start = (char *)shmat(shmid, NULL, 0);
if ((void *)start == (void *)-1)
{
perror("shmat");
exit(1);
}
// 연결에 성공하면 3초 동안 절전 모드로 전환됩니다.
printf("start: %p
", start);
sleep(3);
return 0;
}
동일한 공유 메모리는 가상 주소이므로 연결된 두 프로세스에서 매핑된 주소가 다릅니다.
프로세스 연결 해제
shmdt 함수 사용
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
주의:
공유 메모리가 제거된 후에도 성공적으로 마운트된 프로세스는 정상적으로 통신할 수 있지만 현재로서는 다른 프로세스를 마운트할 수 없습니다.
공유 메모리가 조기에 삭제되면 상태가 파괴된 dest로 변경됩니다.
공유 메모리 제어
shmctl 함수
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
/*
* cmd: 공유 메모리를 제어하기 위한 구체적인 작업
* -IPC_STAT 제어 중인 공유 메모리를 가져오거나 설정하는 데 사용되는 데이터 구조체입니다.
* -IPC_SET 프로세스에 충분한 권한이 있는 경우 공유 메모리의 현재 연관 값을 BUF 구조의 값으로 설정합니다.
* -IPC_RMID 공유 메모리 해제
* bufPOSIX 버전의 세마포어는 POSIX 버전의 세마포어보다 훨씬 간단하고 널리 사용됩니다._RMID멀티스레딩을 구현하기 위해
*/
shmctl을 통해 공유 메모리의 데이터 구조를 가져오고 거기에서 키인 pid를 가져옵니다.
#include <iostream>
#include "common.hpp"
using namespace std;
int main()
{
// 서버 측에서 공유 메모리 만들기
key_t key = getKey();
int shmid = createShm(key, gsize);
char *start = (char *)shmat(shmid, NULL, 0);
if ((void *)start == (void *)-1){
perror("shmat");
shmctl(shmid, IPC_RMID, NULL);
exit(1);
}
struct shmid_ds buf; // 공유 메모리 데이터 구조
int n = shmctl(shmid, IPC_STAT, &buf);
if (n == -1){
perror("shmctl");
shmctl(shmid, IPC_RMID, NULL);
}
cout << "buf.shm_cpid: " << buf.shm_cpid << endl;
cout << "buf.shm_perm.__key: " << toHEX(buf.shm_perm.__key) << endl;
shmdt(start); //
shmctl(shmid, IPC_RMID, NULL);// 공유 메모리 해제
return 0;
}
공유 메모리 크기
위 코드에서 공유 메모리의 크기는 PAGE 페이지 크기인 4096바이트로 설정되어 있으며, 4097바이트의 공유 메모리를 요청하면 운영 체제에서 실제로 8192바이트를 할당하지만 공유 메모리 사용에는 4097바이트만 사용할 수 있습니다.
때문에, 공유 메모리가 주로 범위를 벗어난 액세스가 발생했는지 감지하는 데 사용되는 경우 추가로 열린 공간은 공유 메모리에 제공되지 않습니다.
공유 메모리가 "빠른" 이유
더 빠른 공유 메모리 IPC의 비결은 데이터 복사를 줄이는 것이며, IO는 매우 느리고 비효율적입니다.
예를 들어 파이프라인 통신을 사용합니다:
- 프로세스 A에서 데이터 읽기
- 파이프라인을 연 다음 시스템 호출을 통해 파이프라인에 데이터를 씁니다.
- 시스템 호출을 통해 파이프라인에서 데이터 읽기
- 읽은 데이터를 프로세스 B로 출력합니다.
하지만 공유 메모리, 데이터 읽기 및 쓰기를 위한 동일한 영역에 대한 직접 액세스
- 프로세스 A가 공유 메모리에 직접 데이터를 쓰는 경우
- 프로세스 B는 공유 메모리에서 직접 데이터를 읽습니다.
운영 체제는 불법적인 연산으로 인한 범위를 벗어난 액세스 문제를 방지하기 위해 페이지 페이지 크기의 정수 배수를 열어둡니다.
공유 메모리 단순 사용량
두 프로세스가 동일한 공유 메모리에 성공적으로 연결되면 해당 영역을 직접 읽고 쓸 수 있습니다.
작업을 더 간결하게 하려면 common.hpp의 코드를 생성, 연결, 연결 해제 등을 한 번에 수행하는 클래스로 캡슐화할 수 있습니다.
common.hpp
#include <iostream>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
using namespace std;
#define PATHNAME "." //
#define PROJID 0x29C // 항목 번호
enum{SERVER = 0,CLIENT = 1};
class shm
{
public:
shm(int id): _id(id){
_key = getKey(); //키 얻기
// ID에 따라 공유 메모리를 생성/개방합니다.
if (_id == SERVER)
_shmid = shmHelper(_key, gsize, IPC_CREAT | IPC_EXCL | mode);
else
_shmid = shmHelper(_key, gsize, IPC_CREAT);
// 연관 공유 메모리
_start = shmat(_shmid, NULL, 0); //
if (_start == (void *)-1){
perror("shmat");
exit(-1);
}
}
~shm(){
//
int n = shmdt(_start);
if (n == -1){
perror("shmdt");
exit(-1);
}
// 사용자의 신원에 따라 공유 메모리를 삭제해야 하는지 여부를 결정합니다.
if (_id == SERVER)
shmctl(_shmid, IPC_RMID, NULL);
}
key_t getKey() const{
key_t key = ftok(PATHNAME, PROJID);
if (key == -1){
perror("ftok");
exit(-1);
}
return key;
}
int getShmID() const{
return _shmid;
}
void *getStart() const{
return _start;
}
protected:
static const int gsize = 4096;
static const mode_t mode = 0666;
// 10진수를 16진수로 변환하기.
string toHEX(int x){
char buffer[64];
snprintf(buffer, sizeof buffer, "0x%x", x);
return buffer;
}
// 공유 메모리 도우미
int shmHelper(key_t key, size_t size, int flags){
int shmid = shmget(key, size, flags);
if (shmid == -1){
perror("shmget");
exit(2);
}
return shmid;
}
private:
key_t _key;
int _shmid = 0;
void *_start;
int _id; // 서버와 클라이언트를 구분하는 식별자.
};
사용: 클라이언트가 서버로 정보를 전송하고, 서버가 정보를 수신하여 표시합니다.
서버 서버
#include <iostream>
#include <cstring>
#include <unistd.h>
#include "common.hpp"
using namespace std;
int main()
{
//
shm s(SERVER);
// 공유 메모리의 시작 주소 얻기
char *start = (char *)s.getStart();
char *before = (char *)malloc(sizeof(char *) * (1024));
strcpy(before, start);
// 통신 시작하기
while (true)
{
if (strcmp(start, "exit") == 0) // 출구를 받으면
break;
if (strcmp(start, before) != 0) // 이 메시지가 이전 메시지와 동일한 경우에는 표시되지 않습니다.
{
cout << "server get: " << start << endl;
strcpy(before, start);
}
sleep(1);
}
free(before);
return 0;
}
클라이언트
#include <iostream>
#include <string>
#include "common.hpp"
using namespace std;
int main()
{
//
shm c(CLIENT);
// 공유 메모리의 시작 주소 가져오기
char *start = (char *)c.getStart();
string s;
// 통신 시작하기
int n = 0;
printf("client sent:
");
//26글자를 쓴 후 클라이언트 종료
while (1)
{
getline(cin, s);
strcpy(start, s.c_str());
if ("exit" == s)
break;
}
return 0;
}
네임드 파이프와 연계한 통신 완료
공유 메모리에는 동기화 및 상호 제외 메커니즘이 없으며, 프로세스에서 데이터를 쓰기 전에 데이터를 읽을 수 있어 데이터가 보호되지 않을 수 있다는 단점이 있습니다.
다른 통신 방법을 사용하여 공유 메모리에 대한 쓰기 및 읽기 규칙을 제어할 수 있는데, 프로세스 A는 프로세스 B에 읽기를 알리기 전에 데이터를 쓰고, 프로세스 B는 프로세스 A에 쓰기를 알리기 전에 읽는 네임드 파이프라인을 사용할 수 있습니다.
필요한 리소스: 공유 메모리 1개, 네임드 파이프 2개
- 하나의 파이프라인이 서버 측 쓰기와 클라이언트 측 읽기를 담당합니다.
- 파이프라인은 서버 측 읽기 및 클라이언트 측 쓰기를 담당하여 간접적으로 양방향 알림을 활성화합니다.
common.hpp는 공유 메모리와 네임드 파이프 준비 작업을 캡슐화합니다.
#include <iostream>
#include <cerrno>
#include <cassert>
#include <cstring>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
#define PATHNAME "." //
#define PROJID 0x29C // 항목 번호
// 두 개의 파이프 이름
const char *fifo_name1 = "fifo1";
const char *fifo_name2 = "fifo2";
enum{SERVER = 0,CLIENT = 1};
class shm
{
public:
shm(int id): _id(id){
_key = getKey(); // 키 얻기
// 서버 측: 공유 메모리 만들기, 네임드 파이프 만들기 및 열기.
if (_id == SERVER){
_shmid = shmHelper(_key, gsize, IPC_CREAT | IPC_EXCL | mode);
int n = mkfifo(fifo_name1, mode);
if(n==-1){
perror("mkfifo");
exit(-1);
}
n = mkfifo(fifo_name2, mode);
if(n==-1){
perror("mkfifo");
exit(-1);
}
// 서버는 쓰기를 위해 명명된 파이프 1을 열고 읽기를 위해 명명된 파이프 2를 엽니다.
_wfd = open(fifo_name1, O_WRONLY);
_rfd = open(fifo_name2, O_RDONLY);
}
// 클라이언트 측: 공유 메모리 열기, 네임드 파이프 열기.
else{
_shmid = shmHelper(_key, gsize, IPC_CREAT);
// 클라이언트는 읽기를 위해 명명된 파이프 1을 열고 쓰기를 위해 명명된 파이프 2를 엽니다.
_rfd = open(fifo_name1, O_RDONLY);
_wfd = open(fifo_name2, O_WRONLY);
}
// 연관 공유 메모리
_start = shmat(_shmid, NULL, 0); //
if (_start == (void *)-1){
perror("shmat");
exit(1);
}
}
~shm(){
// fd 닫기
close(_wfd);
close(_rfd);
//
int n = shmdt(_start);
if (n == -1){
perror("shmdt");
exit(-1);
}
// 파이프라인 파일 삭제, 공유 메모리 삭제하기
if (_id == SERVER){
unlink(fifo_name1);
unlink(fifo_name2);
shmctl(_shmid, IPC_RMID, NULL);
}
}
key_t getKey() const{
key_t key = ftok(PATHNAME, PROJID);
if (key == -1){
perror("ftok");
exit(-1);
}
return key;
}
int getShmID() const{
return _shmid;
}
void *getStart() const{
return _start;
}
int getWFD() const{
return _wfd;
}
int getRFD() const{
return _rfd;
}
protected:
static const int gsize = 4096;
static const mode_t mode = 0666;
// 공유 메모리 도우미
int shmHelper(key_t key, size_t size, int flags){
int shmid = shmget(key, size, flags);
if (shmid == -1){
perror("shmget");
exit(-1);
}
return shmid;
}
private:
key_t _key;
int _shmid = 0;
void *_start;
int _wfd; // 쓰기 측과 읽기 측 fd 비교
int _rfd;
int _id; // 서버와 클라이언트를 구분하는 식별자.
};
서버 읽기 측에서는 먼저 파이프라인 2를 사용하여 클라이언트에 쓰기가 가능하다는 것을 알린 다음 공유 메모리에서 메시지를 가져옵니다.
#include <iostream>
#include <cstring>
#include <unistd.h>
#include "common.hpp"
using namespace std;
int main(){
// 서버 측: 읽기
shm s(SERVER);
char *start = (char *)s.getStart();
int wfd = s.getWFD();
int rfd = s.getRFD();
const char *str = "yes";
// 여기서는 서버가 먼저 시작되고 파이프라인에 yes 명령을 실행하여 클라이언트가 공유 메모리에 직접 쓸 수 있음을 나타냅니다.
write(wfd, str, strlen(str));
char buff[64];
while (true){
// 클라이언트의 작업 명령 대기
// 파이프라인의 동기화 메커니즘으로 인해 읽기 측에서 파이프라인에서 데이터를 읽을 때 데이터가 없으면 쓰기 측에서 데이터를 쓸 때까지 차단하고 기다립니다.
int n = read(rfd, buff, sizeof(buff) - 1);
buff[n] = 0;
if (n > 0){
if (strcasecmp(str, buff) == 0){
// 클라이언트는 서버가
int i = 0;
while (start[i]){
buff[i] = start[i];
i++;
}
buff[i] = 0;
printf("server read: %s
", buff);
if (strcasecmp("exit", buff) == 0)
break;
// 읽기가 완료되면 클라이언트에게 쓰기를 알립니다.
write(wfd, str, strlen(str));
}
}
else if (n == 0)
cerr << "클라이언트가 파이프라인에서 읽지 않았습니다." << endl;
else{
cerr << "읽기 예외" << endl;
break;
}
}
return 0;
}
클라이언트 쓰기 측에서는 서버가 공유 메모리에 쓸 수 있다는 알림을 받은 후 공유 메모리에 메시지를 쓴 다음 파이프라인 1을 사용하여 서버가 이를 읽도록 알립니다.
#include <iostream>
#include <cstring>
#include <unistd.h>
#include "common.hpp"
using namespace std;
int main(){
// 클라이언트: 쓰기
shm c(CLIENT);
char *start = (char *)c.getStart();
int wfd = c.getWFD();
int rfd = c.getRFD();
const char *str = "yes";
srand((size_t)time(NULL));
char buff[64];
while (true){
// 서버에서 작업 명령 대기 중
int n = read(rfd, buff, sizeof(buff) - 1);
buff[n] = 0;
if (n > 0){
if (strcasecmp(str, buff) == 0){
printf("client write: ");
fflush(stdout);
fgets(buff, sizeof buff, stdin);
int i = 0;
while (buff[i] != '
'){
start[i] = buff[i];
i++;
}
buff[i] = start[i] = 0;
// 쓰기가 완료되면 서버에 읽기를 알립니다.
write(wfd, str, strlen(str));
if (strcasecmp("exit", buff) == 0)
break;
}
}
else if (n == 0)
cerr << "클라이언트가 파이프라인에서 읽지 않았습니다." << endl;
else{
cerr << "읽기 예외" << endl;
break;
}
}
return 0;
}
여기서 시뮬레이션은 클라이언트가 쓰고, 서버가 읽고, 반대로하려면 읽기 / 쓰기 로직을 변경할 수 있습니다. 공유 메모리가 양방향 통신을 지원하기 때문에 두 개의 명명 된 파이프 라인으로 인해 다른 쪽의 시작 부분에서 쓰기 데이터를 기다리고 있으므로 먼저 공격하는 당사자가되어야하며,이 무한 대기를 깨려면 읽기중인 사람, 즉 쓰기 측에 알리기 전에 통신 코드 실행에서 먼저 통지받은 사람이 데이터를 쓸 수 있도록하는 것이 좋습니다.
、메시지 대기열
메시지 큐는 특별한 통신 방법이며 데이터 읽기 및 쓰기를위한 공간의 도움으로 파이프 라인 및 공유 메모리와 다르지만 대기열을 만드는 시스템 에서이 큐의 노드는 유형과 정보를 포함하는 데이터 블록입니다.
메시지 큐는 사용하기 번거롭고 너무 쓸모없어 요즘에는 잘 사용되지 않으므로 여기서는 원리를 자세히 설명하지 않겠습니다.
메시지 큐의 인터페이스는 대부분 공유 메모리에 가깝기 때문에 메시지 큐를 빠르게 시작할 수 있습니다.
신호
신호는 동기화 및 상호 배제를 구현하는 데 사용되는 특별한 도구입니다.
상호 제외 개념
1. 동시성이란 시스템에 여러 개의 독립적인 활동 단위가 동시에 존재하는 것을 말합니다.
- 예를 들어 멀티스레딩에서는 여러 실행 스트림이 동시에 코드를 실행하고 동일한 공유 리소스에 액세스할 수 있습니다.
2. 상호 제외는 동시에 하나의 활동 단위만 공유 리소스를 사용할 수 있도록 허용하는 것을 의미합니다.
- 즉, 특정 순간에는 하나의 실행 흐름만 공유 리소스에 액세스할 수 있습니다.
3, 중요 리소스 및 중요 영역, 다중 실행 흐름 환경의 공유 리소스는 중요 리소스이며, 중요 리소스 작업과 관련된 코드 간격은 중요 영역입니다.
- 멀티스레드 환경에서 전역 변수는 중요한 리소스이며, 전역 변수를 수정하고 액세스하는 코드는 중요한 영역에 속합니다.
4. 원자성: 성공과 실패의 두 가지 상태만 존재할 수 있습니다.
- 예를 들어 변수를 수정하면 수정이 성공하거나 실패하며, 수정이 반쯤 잘린 상태는 없습니다.
뮤텍스는 멀티스트림 환경에서 중요한 리소스에 대한 동시 액세스 문제를 해결하는 데 사용되며, 원자 연산을 수행하려면 뮤텍스 잠금 또는 세마포어와 같은 도구가 필요합니다.
작동 방식
세마포어는 주로 여러 프로세스 간 또는 프로세스 내의 여러 스레드 간에 중요 리소스에 대한 액세스를 제어하는 데 사용되는 카운터로, 메모리의 플래그에 해당하며 프로세스가 특정 중요 리소스에 액세스할 수 있는지 여부를 결정할 수 있고 동시에 프로세스는 프로세스 동기화 외에도 중요 리소스의 액세스 제어에 사용되는 플래그를 수정할 수도 있습니다.
구체적인 성능은 리소스 요청, 카운터 -1, 리소스 반환, 카운터 +1, 카운터가 0이 아닌 경우에만 리소스 요청을 수행 할 수 있으며, 상호 배제를 달성하기 위해 이진 신호를 설계 할 수 있습니다.
시스템 V의 신호는 작동하기가 약간 까다롭고, 멀티스레딩을 배우면 훨씬 간단하고 널리 사용되는 상호 제외를 구현하기 위해 POSIX의 신호를 사용하게 됩니다.





