프로세스 간 통신과 시그널
프로세스 간 통신과 시그널¶
목표¶
- IPC 메커니즘 이해하기: 파이프(Pipe), FIFO, 공유 메모리(Shared Memory), 메시지 큐(Message Queue)
- 강력한 프로세스 제어를 위한 sigaction을 사용한 시그널(Signal) 처리 마스터하기
- 생산자-소비자(Producer-Consumer) 및 부모-자식 간 협력을 위한 IPC 패턴 적용하기
난이도: ⭐⭐⭐⭐ (고급)
목차¶
1. 파이프¶
1.1 익명 파이프¶
파이프는 관련된 프로세스(부모-자식) 간에 단방향 데이터 흐름을 제공합니다.
┌────────────────────────────────────────────────────────┐
│ Pipe Communication │
├────────────────────────────────────────────────────────┤
│ │
│ ┌───────────┐ pipe ┌───────────┐ │
│ │ Parent │───────────▶│ Child │ │
│ │ (Writer) │ fd[1] │ (Reader) │ │
│ │ │ ────────▶ │ │ │
│ └───────────┘ fd[0] └───────────┘ │
│ │
│ pipe(fd) creates: │
│ fd[0] = read end │
│ fd[1] = write end │
│ │
└────────────────────────────────────────────────────────┘
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
int pipefd[2];
if (pipe(pipefd) < 0) {
perror("pipe");
exit(EXIT_FAILURE);
}
pid_t pid = fork();
if (pid < 0) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
// Child: read from pipe
close(pipefd[1]); // Close unused write end
char buffer[256];
ssize_t n = read(pipefd[0], buffer, sizeof(buffer) - 1);
if (n > 0) {
buffer[n] = '\0';
printf("Child received: %s\n", buffer);
}
close(pipefd[0]);
exit(EXIT_SUCCESS);
} else {
// Parent: write to pipe
close(pipefd[0]); // Close unused read end
const char *msg = "Hello from parent!";
write(pipefd[1], msg, strlen(msg));
close(pipefd[1]);
wait(NULL); // Wait for child
}
return 0;
}
1.2 두 개의 파이프를 사용한 양방향 통신¶
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
int parent_to_child[2], child_to_parent[2];
pipe(parent_to_child);
pipe(child_to_parent);
pid_t pid = fork();
if (pid == 0) {
// Child
close(parent_to_child[1]);
close(child_to_parent[0]);
char buf[256];
ssize_t n = read(parent_to_child[0], buf, sizeof(buf) - 1);
buf[n] = '\0';
printf("Child got: %s\n", buf);
const char *reply = "Got it, thanks!";
write(child_to_parent[1], reply, strlen(reply));
close(parent_to_child[0]);
close(child_to_parent[1]);
exit(0);
}
// Parent
close(parent_to_child[0]);
close(child_to_parent[1]);
const char *msg = "Task: process data";
write(parent_to_child[1], msg, strlen(msg));
close(parent_to_child[1]);
char buf[256];
ssize_t n = read(child_to_parent[0], buf, sizeof(buf) - 1);
buf[n] = '\0';
printf("Parent got reply: %s\n", buf);
close(child_to_parent[0]);
wait(NULL);
return 0;
}
1.3 exec와 함께 사용하는 파이프 (셸 파이핑)¶
// Simulate: ls -la | grep ".c"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
int pipefd[2];
pipe(pipefd);
pid_t pid1 = fork();
if (pid1 == 0) {
// First child: ls -la
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO); // stdout → pipe write
close(pipefd[1]);
execlp("ls", "ls", "-la", NULL);
perror("execlp ls");
exit(1);
}
pid_t pid2 = fork();
if (pid2 == 0) {
// Second child: grep ".c"
close(pipefd[1]);
dup2(pipefd[0], STDIN_FILENO); // stdin ← pipe read
close(pipefd[0]);
execlp("grep", "grep", ".c", NULL);
perror("execlp grep");
exit(1);
}
// Parent: close both ends and wait
close(pipefd[0]);
close(pipefd[1]);
waitpid(pid1, NULL, 0);
waitpid(pid2, NULL, 0);
return 0;
}
2. 명명된 파이프 (FIFOs)¶
FIFO는 파일 시스템 엔트리를 통해 관련 없는 프로세스 간 통신을 가능하게 합니다.
2.1 FIFO 생성 및 사용¶
// --- Writer process ---
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#define FIFO_PATH "/tmp/myfifo"
int main(void) {
// Create FIFO (ignore error if it already exists)
mkfifo(FIFO_PATH, 0666);
int fd = open(FIFO_PATH, O_WRONLY);
if (fd < 0) {
perror("open");
exit(1);
}
const char *messages[] = {"Hello", "World", "Done"};
for (int i = 0; i < 3; i++) {
write(fd, messages[i], strlen(messages[i]) + 1);
printf("Sent: %s\n", messages[i]);
sleep(1);
}
close(fd);
return 0;
}
// --- Reader process ---
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#define FIFO_PATH "/tmp/myfifo"
int main(void) {
int fd = open(FIFO_PATH, O_RDONLY);
if (fd < 0) {
perror("open");
exit(1);
}
char buffer[256];
ssize_t n;
while ((n = read(fd, buffer, sizeof(buffer))) > 0) {
printf("Received: %s\n", buffer);
}
close(fd);
unlink(FIFO_PATH); // Clean up
return 0;
}
3. 공유 메모리¶
공유 메모리는 프로세스 간에 데이터를 복사할 필요가 없기 때문에 가장 빠른 IPC 메커니즘입니다.
3.1 POSIX 공유 메모리¶
┌──────────────────────────────────────────────────────────┐
│ Shared Memory Architecture │
├──────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ Physical Memory ┌─────────┐│
│ │ Process A │ ┌──────────────┐ │Process B ││
│ │ │ │ │ │ ││
│ │ Virtual │──────▶│ Shared │◀─────│ Virtual ││
│ │ Address │ mmap │ Region │ mmap │ Address ││
│ │ 0x7f... │ │ │ │ 0x7f... ││
│ │ │ └──────────────┘ │ ││
│ └─────────────┘ └─────────┘│
│ │
│ ⚠ Requires synchronization (semaphore/mutex) │
│ │
└──────────────────────────────────────────────────────────┘
// --- Producer ---
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <semaphore.h>
#define SHM_NAME "/my_shm"
#define SEM_NAME "/my_sem"
#define SHM_SIZE 4096
typedef struct {
int count;
char data[256];
} shared_data_t;
int main(void) {
// Create shared memory
int shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
ftruncate(shm_fd, sizeof(shared_data_t));
shared_data_t *shm = mmap(NULL, sizeof(shared_data_t),
PROT_READ | PROT_WRITE,
MAP_SHARED, shm_fd, 0);
// Create semaphore for synchronization
sem_t *sem = sem_open(SEM_NAME, O_CREAT, 0666, 0);
// Write data
shm->count = 42;
snprintf(shm->data, sizeof(shm->data),
"Hello from producer (PID=%d)", getpid());
printf("Producer wrote: count=%d, data=%s\n",
shm->count, shm->data);
// Signal consumer
sem_post(sem);
// Cleanup
sem_close(sem);
munmap(shm, sizeof(shared_data_t));
close(shm_fd);
return 0;
}
// --- Consumer ---
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <semaphore.h>
#define SHM_NAME "/my_shm"
#define SEM_NAME "/my_sem"
typedef struct {
int count;
char data[256];
} shared_data_t;
int main(void) {
// Open shared memory
int shm_fd = shm_open(SHM_NAME, O_RDONLY, 0666);
shared_data_t *shm = mmap(NULL, sizeof(shared_data_t),
PROT_READ, MAP_SHARED, shm_fd, 0);
// Wait for producer
sem_t *sem = sem_open(SEM_NAME, 0);
sem_wait(sem);
// Read data
printf("Consumer read: count=%d, data=%s\n",
shm->count, shm->data);
// Cleanup
sem_close(sem);
sem_unlink(SEM_NAME);
munmap(shm, sizeof(shared_data_t));
close(shm_fd);
shm_unlink(SHM_NAME);
return 0;
}
4. POSIX 메시지 큐¶
메시지 큐는 우선순위를 지원하는 구조화된 메시지 전달을 제공합니다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <mqueue.h>
#include <fcntl.h>
#define QUEUE_NAME "/my_queue"
#define MAX_MSG_SIZE 256
#define MAX_MSGS 10
// Sender
void sender(void) {
struct mq_attr attr = {
.mq_flags = 0,
.mq_maxmsg = MAX_MSGS,
.mq_msgsize = MAX_MSG_SIZE,
.mq_curmsgs = 0
};
mqd_t mq = mq_open(QUEUE_NAME, O_CREAT | O_WRONLY, 0666, &attr);
if (mq == (mqd_t)-1) {
perror("mq_open");
exit(1);
}
const char *msgs[] = {"High priority!", "Normal message", "Low priority"};
unsigned int priorities[] = {10, 5, 1};
for (int i = 0; i < 3; i++) {
mq_send(mq, msgs[i], strlen(msgs[i]) + 1, priorities[i]);
printf("Sent (prio=%u): %s\n", priorities[i], msgs[i]);
}
mq_close(mq);
}
// Receiver
void receiver(void) {
mqd_t mq = mq_open(QUEUE_NAME, O_RDONLY);
if (mq == (mqd_t)-1) {
perror("mq_open");
exit(1);
}
char buffer[MAX_MSG_SIZE];
unsigned int priority;
// Messages arrive highest priority first
for (int i = 0; i < 3; i++) {
ssize_t bytes = mq_receive(mq, buffer, MAX_MSG_SIZE, &priority);
if (bytes >= 0) {
printf("Received (prio=%u): %s\n", priority, buffer);
}
}
mq_close(mq);
mq_unlink(QUEUE_NAME);
}
5. 시그널¶
5.1 시그널 개요¶
시그널은 프로세스에 이벤트를 알리기 위해 전달되는 소프트웨어 인터럽트입니다.
┌──────────────────────────────────────────────────────────┐
│ Common Signals │
├─────────┬──────────────────────────────────────────────┤
│ Signal │ Description │
├─────────┼──────────────────────────────────────────────┤
│ SIGINT │ Interrupt (Ctrl+C) │
│ SIGTERM │ Termination request │
│ SIGKILL │ Forced kill (cannot be caught) │
│ SIGCHLD │ Child process stopped or terminated │
│ SIGUSR1 │ User-defined signal 1 │
│ SIGUSR2 │ User-defined signal 2 │
│ SIGALRM │ Timer alarm │
│ SIGPIPE │ Broken pipe (write to closed socket) │
│ SIGSEGV │ Segmentation fault │
│ SIGSTOP │ Stop process (cannot be caught) │
│ SIGCONT │ Continue stopped process │
└─────────┴──────────────────────────────────────────────┘
5.2 sigaction을 사용한 시그널 처리¶
이식성과 신뢰성 있는 동작을 위해 signal() 대신 항상 sigaction()을 사용하세요.
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
volatile sig_atomic_t running = 1;
void handle_sigint(int sig) {
(void)sig; // Suppress unused warning
running = 0;
// Only async-signal-safe functions here!
write(STDOUT_FILENO, "\nCaught SIGINT, shutting down...\n", 33);
}
void handle_sigusr1(int sig, siginfo_t *info, void *context) {
(void)sig;
(void)context;
// siginfo_t gives us sender information
printf("SIGUSR1 from PID %d\n", info->si_pid);
}
int main(void) {
// Setup SIGINT handler
struct sigaction sa_int = {0};
sa_int.sa_handler = handle_sigint;
sigemptyset(&sa_int.sa_mask);
sa_int.sa_flags = 0;
sigaction(SIGINT, &sa_int, NULL);
// Setup SIGUSR1 handler with siginfo
struct sigaction sa_usr = {0};
sa_usr.sa_sigaction = handle_sigusr1;
sigemptyset(&sa_usr.sa_mask);
sa_usr.sa_flags = SA_SIGINFO;
sigaction(SIGUSR1, &sa_usr, NULL);
// Ignore SIGPIPE (common in network programs)
signal(SIGPIPE, SIG_IGN);
printf("PID: %d - Press Ctrl+C or send SIGUSR1\n", getpid());
while (running) {
printf("Working...\n");
sleep(2);
}
printf("Clean shutdown complete\n");
return 0;
}
5.3 시그널 마스킹¶
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
int main(void) {
sigset_t block_set, old_set;
// Block SIGINT during critical section
sigemptyset(&block_set);
sigaddset(&block_set, SIGINT);
sigprocmask(SIG_BLOCK, &block_set, &old_set);
// ---- Critical section ----
printf("SIGINT blocked. Ctrl+C won't interrupt.\n");
sleep(5);
printf("Critical section done.\n");
// ---- End critical section ----
// Restore original mask
sigprocmask(SIG_SETMASK, &old_set, NULL);
printf("SIGINT unblocked. Ctrl+C works again.\n");
sleep(5);
return 0;
}
5.4 SIGCHLD를 사용한 자식 프로세스 정리¶
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>
void handle_sigchld(int sig) {
(void)sig;
// Reap all terminated children (non-blocking)
int status;
pid_t pid;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
if (WIFEXITED(status)) {
// Child exited normally
}
}
}
int main(void) {
struct sigaction sa = {0};
sa.sa_handler = handle_sigchld;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa, NULL);
// Fork multiple children
for (int i = 0; i < 5; i++) {
pid_t pid = fork();
if (pid == 0) {
printf("Child %d (PID=%d) working...\n", i, getpid());
sleep(i + 1);
printf("Child %d done\n", i);
exit(i);
}
}
// Parent continues working
printf("Parent (PID=%d) waiting...\n", getpid());
sleep(10);
printf("Parent done\n");
return 0;
}
6. 연습 문제¶
문제 1: 공유 메모리를 사용한 생산자-소비자¶
POSIX 공유 메모리와 세마포어를 사용하여 생산자-소비자 시스템을 구현하세요: - 생산자는 공유 메모리의 순환 버퍼에 정수 1-100을 씁니다 - 소비자는 그것을 읽고 출력합니다 - 동기화를 위해 세마포어를 사용합니다
문제 2: 다중 프로세스 파이프라인¶
파이프를 사용하여 3단계 파이프라인을 만드세요: - 1단계: 파일에서 줄을 읽습니다 - 2단계: 대문자로 변환합니다 - 3단계: 단어 빈도를 계산하고 출력합니다
문제 3: 워치독 프로세스¶
다음 기능을 가진 워치독을 작성하세요: - 자식 워커 프로세스를 포크합니다 - SIGCHLD로 모니터링합니다 - 충돌 시 자동으로 재시작합니다 - 두 프로세스 모두의 우아한 종료를 위해 SIGTERM을 처리합니다
7. 참고 자료¶
- W. Richard Stevens, Advanced Programming in the UNIX Environment (3rd ed.)
man 7 pipe,man 7 fifo,man 7 shm_overview,man 7 mq_overviewman 7 signal,man 2 sigaction,man 2 sigprocmask