# 파이프와 소켓을 통한 통신 방법

Preempt RT에서 실시간 시스템에서의 통신은 매우 중요한 요소이다. 특히, 파이프(pipe)와 소켓(socket)은 프로세스 간 통신(IPC)을 위한 강력한 도구로 사용된다. 이 장에서는 파이프와 소켓을 활용하여 실시간 데이터를 교환하는 방법에 대해 자세히 설명한다.

#### 파이프를 통한 통신

파이프는 두 프로세스 간에 데이터를 전송할 수 있는 일방향 또는 양방향 통신 채널이다. 파이프를 사용하면 하나의 프로세스가 데이터를 쓰고 다른 프로세스가 이를 읽는 방식으로 데이터를 교환할 수 있다.

**파이프의 기본 개념**

파이프는 커널에 의해 관리되는 버퍼로, 두 프로세스 간에 데이터를 전달하기 위해 사용된다. 파이프는 크게 다음 두 가지 형태로 구분된다:

1. **익명 파이프(Anonymous Pipe)**: 부모-자식 프로세스 간에만 사용할 수 있으며, 주로 단일 머신 내에서 프로세스 간 통신에 사용된다.
2. **이름 있는 파이프(Named Pipe, FIFO)**: 서로 관련 없는 프로세스 간에도 사용할 수 있으며, 파이프에 이름이 붙어 있어 시스템의 모든 프로세스가 접근할 수 있다.

**익명 파이프의 사용법**

익명 파이프는 주로 `pipe()` 시스템 호출을 통해 생성된다. 이 함수는 두 개의 파일 디스크립터를 포함하는 배열을 반환한다. 하나는 파이프의 읽기 끝(read end)을 가리키고, 다른 하나는 쓰기 끝(write end)을 가리킨다.

```c
int fd[2];
pipe(fd);
```

위 코드에서 `fd[0]`은 읽기 끝을, `fd[1]`은 쓰기 끝을 나타낸다. 데이터를 쓰려면 `write(fd[1], data, size)`를 사용하고, 읽으려면 `read(fd[0], buffer, size)`를 사용한다.

파이프를 사용한 통신에서 중요한 점은, 실시간성을 보장하기 위해 파이프의 크기와 데이터 전송 속도에 주의를 기울여야 한다는 것이다. 특히, 파이프가 가득 차거나 비어 있을 때 발생할 수 있는 블로킹(blocking) 현상을 피하기 위해서는 비동기 I/O와 같은 기법을 사용하는 것이 필요하다.

**이름 있는 파이프의 사용법**

이름 있는 파이프는 `mkfifo()` 시스템 호출을 통해 생성된다. 이 파이프는 파일 시스템의 특별한 파일로 간주되며, 여러 프로세스가 이 파일을 통해 통신할 수 있다.

```c
mkfifo("/tmp/my_fifo", 0666);
```

위 명령은 `/tmp/my_fifo`라는 이름의 FIFO 파일을 생성한다. 이 파일을 통해 통신하려는 프로세스들은 일반적인 파일 입출력 함수(`open()`, `read()`, `write()` 등)를 사용하여 데이터를 교환할 수 있다.

#### 소켓을 통한 통신

소켓은 네트워크를 통해 데이터 통신을 할 수 있는 더 강력하고 유연한 도구이다. 소켓을 사용하면 같은 호스트 내의 프로세스 간 통신뿐만 아니라, 다른 호스트에 있는 프로세스와도 통신할 수 있다.

**소켓의 기본 개념**

소켓은 IP 주소와 포트 번호를 기반으로 네트워크 상에서 데이터를 송수신할 수 있게 해주는 인터페이스이다. 소켓을 생성하고 사용하기 위해서는 다음과 같은 절차를 거친다:

1. **소켓 생성**: `socket()` 함수로 소켓을 생성한다.
2. **주소 지정**: `bind()` 함수로 소켓에 IP 주소와 포트 번호를 지정한다.
3. **연결 대기**: 서버 측에서 `listen()` 함수를 통해 클라이언트의 연결 요청을 대기한다.
4. **연결 수락**: 클라이언트의 연결 요청이 들어오면 `accept()` 함수로 이를 수락한다.
5. **데이터 송수신**: `send()`와 `recv()` 함수로 데이터를 송수신한다.
6. **소켓 종료**: 통신이 끝나면 `close()` 함수로 소켓을 종료한다.

**TCP 소켓의 사용법**

TCP(Transmission Control Protocol) 소켓은 신뢰성 있는 연결 지향형 통신을 제공한다. TCP 소켓을 사용한 통신의 간단한 예제는 다음과 같다.

**서버 측:**

```c
int server_fd, client_fd;
struct sockaddr_in address;
int addrlen = sizeof(address);

// 소켓 생성
server_fd = socket(AF_INET, SOCK_STREAM, 0);

// 주소 및 포트 지정
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);

bind(server_fd, (struct sockaddr *)&address, sizeof(address));

// 연결 대기
listen(server_fd, 3);

// 연결 수락
client_fd = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);

// 데이터 송수신
read(client_fd, buffer, size);
write(client_fd, data, size);
```

**클라이언트 측:**

```c
int sock = 0;
struct sockaddr_in serv_addr;

// 소켓 생성
sock = socket(AF_INET, SOCK_STREAM, 0);

serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);

connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

// 데이터 송수신
send(sock, data, size, 0);
recv(sock, buffer, size, 0);
```

TCP 소켓은 연결이 수립된 후 데이터가 손실되지 않고 순서대로 도착하도록 보장한다. 그러나 이러한 안정성 때문에 약간의 오버헤드가 발생할 수 있으며, 이는 실시간 시스템에서 주의해야 할 점이다.

**UDP 소켓의 사용법**

UDP(User Datagram Protocol) 소켓은 비연결 지향형 통신을 제공한다. 이는 TCP와 달리 데이터 전송을 보장하지 않으며, 패킷이 손실되거나 순서가 뒤바뀔 수 있다. 그러나 이러한 특성 덕분에 오버헤드가 적고, 실시간 성능을 더 잘 보장할 수 있다.

**서버 측:**

```c
int server_fd;
struct sockaddr_in server_addr, client_addr;
char buffer[1024];
socklen_t addr_len = sizeof(client_addr);

// 소켓 생성
server_fd = socket(AF_INET, SOCK_DGRAM, 0);

// 주소 및 포트 지정
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);

bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));

// 데이터 수신
recvfrom(server_fd, buffer, 1024, MSG_WAITALL, (struct sockaddr *)&client_addr, &addr_len);

// 데이터 전송
sendto(server_fd, data, size, MSG_CONFIRM, (struct sockaddr *)&client_addr, addr_len);
```

**클라이언트 측:**

```c
int sock;
struct sockaddr_in serv_addr;
char buffer[1024];
socklen_t addr_len = sizeof(serv_addr);

// 소켓 생성
sock = socket(AF_INET, SOCK_DGRAM, 0);

serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);

// 데이터 전송
sendto(sock, data, size, MSG_CONFIRM, (struct sockaddr *)&serv_addr, addr_len);

// 데이터 수신
recvfrom(sock, buffer, 1024, MSG_WAITALL, (struct sockaddr *)&serv_addr, &addr_len);
```

UDP 소켓은 데이터 전송이 매우 빠르고, 연결 설정 및 유지 비용이 없기 때문에 실시간 요구사항이 높은 시스템에서 유용하다. 그러나 데이터 신뢰성을 보장해야 하는 경우 추가적인 프로토콜이나 검증 절차를 구현해야 한다.

#### 파이프와 소켓의 비교

파이프와 소켓은 둘 다 프로세스 간 통신(IPC)에 사용되지만, 그 사용 목적과 특성은 크게 다르다. 실시간 시스템에서 파이프와 소켓을 선택할 때는 다음과 같은 점들을 고려해야 한다.

* **통신 범위**: 파이프는 일반적으로 같은 시스템 내의 프로세스 간 통신에 사용되며, 소켓은 네트워크를 통해 다른 시스템 간 통신도 가능한다.
* **통신 방식**: 파이프는 양방향 통신이 가능하지만, 데이터가 순차적으로 전달된다는 점에서 제한이 있을 수 있다. 소켓은 UDP를 통해 비연결형 통신도 지원하며, TCP를 통해 신뢰성 있는 연결형 통신도 지원한다.
* **오버헤드**: 소켓, 특히 TCP 소켓은 데이터 전송의 신뢰성을 보장하기 위해 더 많은 오버헤드가 발생할 수 있다. 반면, 파이프는 시스템 내에서 빠른 데이터 전송을 지원하므로 오버헤드가 적다.

이와 같은 특성을 이해하고, 실시간 시스템에서 요구되는 통신의 속도, 신뢰성, 범위 등을 고려하여 적절한 통신 방법을 선택해야 한다.

#### 실시간 시스템에서의 비동기 통신

실시간 시스템에서는 통신 채널이 블로킹되는 것을 방지하기 위해 비동기 통신을 구현하는 것이 중요하다. 파이프와 소켓 모두 비동기 I/O 방식으로 구현할 수 있으며, 이를 통해 실시간성을 더욱 강화할 수 있다.

**비동기 I/O 개념**

비동기 I/O는 프로세스가 I/O 작업을 요청한 후 해당 작업이 완료되기를 기다리지 않고, 즉시 다음 작업을 수행할 수 있도록 한다. 이는 실시간 시스템에서 매우 중요한데, 이는 프로세스가 일정 시간 내에 응답해야 하는 경우가 많기 때문이다.

비동기 I/O를 구현하기 위해서는 다음과 같은 기술을 사용할 수 있다:

* **Non-blocking I/O**: 파이프나 소켓을 `fcntl()` 함수나 `ioctl()` 함수 등을 사용해 논블로킹 모드로 설정하여, I/O 호출이 즉시 반환되도록 할 수 있다.
* **Multiplexing**: `select()`, `poll()`, `epoll()` 등을 사용해 여러 I/O 채널을 동시에 모니터링하고, 사용할 수 있는 채널에서만 데이터를 송수신할 수 있도록 한다.
* **Signals**: 특정 I/O 이벤트가 발생했을 때, 프로세스에 신호를 보내는 방식으로 비동기 처리를 구현할 수 있다.

**Non-blocking I/O 구현 예제**

파이프와 소켓에서 비동기 I/O를 구현하는 간단한 예제를 보겠다.

**Non-blocking 소켓 설정:**

```c
int flags = fcntl(sock_fd, F_GETFL, 0);
fcntl(sock_fd, F_SETFL, flags | O_NONBLOCK);
```

이 설정을 통해 소켓 `sock_fd`는 비동기 모드에서 작동하게 된다. 이제 이 소켓에서 데이터를 송수신할 때, 데이터가 준비되지 않았더라도 호출이 즉시 반환된다.

**Multiplexing을 통한 비동기 통신**

Multiplexing 기법은 하나의 프로세스가 여러 개의 파일 디스크립터(소켓, 파이프 등)를 동시에 감시할 수 있게 한다. 이는 실시간 시스템에서 여러 I/O 소스에서 오는 데이터를 효과적으로 처리할 수 있게 해준다.

**`select()` 함수의 사용법:**

`select()` 함수는 주어진 시간 동안 하나 이상의 파일 디스크립터에서 데이터가 준비될 때까지 대기한다. 준비된 파일 디스크립터를 확인한 후, 해당 디스크립터에서 I/O 작업을 수행할 수 있다.

```c
fd_set readfds;
struct timeval tv;
int retval;

// 파일 디스크립터 집합 초기화
FD_ZERO(&readfds);
FD_SET(sock_fd, &readfds);

// 타임아웃 설정 (2초)
tv.tv_sec = 2;
tv.tv_usec = 0;

// 파일 디스크립터를 감시
retval = select(sock_fd + 1, &readfds, NULL, NULL, &tv);

if (retval == -1) {
    perror("select()");
} else if (retval) {
    // 데이터가 준비됨
    read(sock_fd, buffer, size);
} else {
    // 타임아웃 발생
    printf("No data within two seconds.\n");
}
```

위의 코드에서 `select()` 함수는 소켓 `sock_fd`에서 데이터가 수신될 때까지 최대 2초 동안 대기한다. 데이터가 수신되면 해당 소켓에서 데이터를 읽을 수 있다.

**`poll()` 함수의 사용법:**

`poll()` 함수는 `select()`와 유사하지만, 파일 디스크립터 집합의 크기에 제한이 없으며 더 많은 기능을 제공한다.

```c
struct pollfd fds[1];
int timeout_msecs = 2000; // 2초

// 감시할 소켓 설정
fds[0].fd = sock_fd;
fds[0].events = POLLIN; // 읽기 가능한지 감시

int ret = poll(fds, 1, timeout_msecs);

if (ret > 0) {
    if (fds[0].revents & POLLIN) {
        // 데이터가 준비됨
        read(sock_fd, buffer, size);
    }
} else if (ret == 0) {
    // 타임아웃 발생
    printf("Poll timed out.\n");
} else {
    perror("poll()");
}
```

`poll()` 함수는 파일 디스크립터가 여러 개일 때 유용하며, 각각의 디스크립터에 대해 다양한 이벤트를 감시할 수 있다.

#### 실시간 통신에서의 우선순위 관리

실시간 시스템에서 통신의 우선순위 관리도 중요한 요소이다. 특정 데이터 전송이 다른 데이터 전송보다 더 중요한 경우, 이를 효과적으로 관리해야 한다. 이를 위해 우선순위 기반 스케줄링이나 큐를 사용할 수 있다.

**우선순위 기반 통신**

우선순위 기반 통신에서는 중요도가 높은 데이터를 먼저 처리할 수 있도록 통신 경로를 설정하거나 프로세스를 스케줄링한다. 예를 들어, 중요한 데이터는 TCP 소켓을 사용해 신뢰성 있는 경로로 전송하고, 덜 중요한 데이터는 UDP 소켓을 통해 빠르게 전송할 수 있다.

또한, 실시간 커널에서 우선순위 기반의 스케줄링 정책을 적용해 높은 우선순위의 프로세스가 I/O 작업을 빠르게 수행할 수 있도록 지원할 수 있다. 이를 통해 실시간 통신의 예측 가능성과 효율성을 높일 수 있다.

#### 공유 메모리와의 통합

파이프와 소켓을 통한 통신 방법은 공유 메모리와 함께 사용할 때 더욱 강력해질 수 있다. 특히, 공유 메모리를 활용하여 파이프나 소켓을 통해 전송되는 데이터를 캐싱하거나, 메모리 맵 파일을 이용해 대용량 데이터를 빠르게 공유할 수 있다.

**예시: 공유 메모리와 소켓 통합**

예를 들어, 실시간 애플리케이션에서 대용량 데이터를 공유 메모리에 저장하고, 소켓을 통해 해당 데이터의 참조나 작은 업데이트를 전송할 수 있다. 이는 네트워크 대역폭을 절약하고, 데이터를 신속하게 접근할 수 있게 한다.

```c
// 공유 메모리 생성
int shm_fd = shm_open("/my_shm", O_CREAT | O_RDWR, 0666);
ftruncate(shm_fd, SHM_SIZE);
void *shm_ptr = mmap(0, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);

// 공유 메모리에 데이터 쓰기
memcpy(shm_ptr, data, data_size);

// 소켓을 통해 업데이트 알림 전송
send(sock_fd, "update", sizeof("update"), 0);
```

위 코드는 공유 메모리에 데이터를 쓰고, 소켓을 통해 다른 프로세스에 업데이트가 발생했음을 알리는 방식이다. 이 접근 방식은 특히 실시간 시스템에서 큰 데이터를 빠르게 처리해야 할 때 유용하다.
