Repository
Mandatory
UNIX 시그널을 사용하여 프로세스 간 통신(IPC)을 구현하는 프로젝트입니다. 클라이언트가 서버에게 문자열을 비트 단위로 전송합니다.
과제 요구사항
두 개의 프로그램 구현:
server: 시작 시 PID를 출력하고 클라이언트로부터 문자열을 받아 출력client: 서버의 PID와 문자열을 인자로 받아 서버에 전송
규칙:
SIGUSR1과SIGUSR2시그널만 사용- 서버는 여러 클라이언트를 연속으로 처리 가능
- 메시지 전송은 빠르게 이루어져야 함
- 통신 속도: 1초에 100자 이상 전송 권장
UNIX 시그널 기초
시그널이란?
프로세스 간 통신(IPC)의 한 방법으로, 운영체제가 프로세스에게 이벤트를 알리는 메커니즘입니다.
주요 시그널:
SIGUSR1(10): 사용자 정의 시그널 1SIGUSR2(12): 사용자 정의 시그널 2SIGINT(2): 인터럽트 (Ctrl+C)SIGTERM(15): 종료 요청SIGKILL(9): 강제 종료 (처리 불가)
시그널 함수
signal()
시그널 핸들러를 등록하는 함수입니다.
#include <signal.h>
void handler(int signum)
{
if (signum == SIGUSR1)
// SIGUSR1 처리
else if (signum == SIGUSR2)
// SIGUSR2 처리
}
int main(void)
{
signal(SIGUSR1, handler); // SIGUSR1 핸들러 등록
signal(SIGUSR2, handler); // SIGUSR2 핸들러 등록
while (1)
pause(); // 시그널 대기
return (0);
}
sigaction() (권장)
signal()보다 안전하고 이식성이 높은 함수입니다.
#include <signal.h>
void handler(int signum, siginfo_t *info, void *context)
{
// info->si_pid로 보낸 프로세스의 PID 확인 가능
if (signum == SIGUSR1)
// SIGUSR1 처리
else if (signum == SIGUSR2)
// SIGUSR2 처리
}
int main(void)
{
struct sigaction sa;
sa.sa_flags = SA_SIGINFO; // siginfo_t 사용
sa.sa_sigaction = handler; // 핸들러 지정
sigemptyset(&sa.sa_mask); // 시그널 마스크 초기화
sigaction(SIGUSR1, &sa, NULL); // SIGUSR1 핸들러 등록
sigaction(SIGUSR2, &sa, NULL); // SIGUSR2 핸들러 등록
while (1)
pause();
return (0);
}
kill()
특정 프로세스에게 시그널을 보내는 함수입니다.
#include <signal.h>
int kill(pid_t pid, int sig);
사용 예:
kill(server_pid, SIGUSR1); // server_pid 프로세스에 SIGUSR1 전송
kill(server_pid, SIGUSR2); // server_pid 프로세스에 SIGUSR2 전송
getpid()
현재 프로세스의 PID를 반환합니다.
#include <unistd.h>
pid_t pid = getpid();
printf("Server PID: %d\n", pid);
pause()
시그널이 올 때까지 프로세스를 일시 정지합니다.
#include <unistd.h>
while (1)
pause(); // 시그널이 올 때까지 대기
usleep()
마이크로초 단위로 프로세스를 일시 정지합니다.
#include <unistd.h>
usleep(100); // 100 마이크로초 (0.0001초) 대기
비트 전송 원리
ASCII 문자를 비트로 표현
문자 ‘A’의 ASCII 코드는 65이고, 이진수로 01000001입니다.
'A' = 65 = 0b01000001
비트 인덱스: 7 6 5 4 3 2 1 0
비트 값: 0 1 0 0 0 0 0 1
비트 단위 전송 방법
전송 규칙:
- 비트가
0이면SIGUSR1전송 - 비트가
1이면SIGUSR2전송 - 최상위 비트(MSB)부터 또는 최하위 비트(LSB)부터 전송
예: ‘A’ (01000001) 전송 (MSB부터)
비트: 0 → SIGUSR1
비트: 1 → SIGUSR2
비트: 0 → SIGUSR1
비트: 0 → SIGUSR1
비트: 0 → SIGUSR1
비트: 0 → SIGUSR1
비트: 0 → SIGUSR1
비트: 1 → SIGUSR2
서버 구현
기본 구조
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
typedef struct s_server
{
unsigned char current_char; // 현재 조립 중인 문자
int bit_count; // 받은 비트 개수
} t_server;
t_server g_server; // 전역 변수 (시그널 핸들러에서 접근 위해)
void signal_handler(int signum)
{
// 비트를 왼쪽으로 시프트
g_server.current_char <<= 1;
// SIGUSR2면 비트를 1로 설정
if (signum == SIGUSR2)
g_server.current_char |= 1;
g_server.bit_count++;
// 8비트를 모두 받으면 문자 출력
if (g_server.bit_count == 8)
{
if (g_server.current_char == '\0')
write(1, "\n", 1); // 문자열 끝
else
write(1, &g_server.current_char, 1);
// 초기화
g_server.current_char = 0;
g_server.bit_count = 0;
}
}
int main(void)
{
pid_t pid;
pid = getpid();
printf("Server PID: %d\n", pid);
// 시그널 핸들러 등록
signal(SIGUSR1, signal_handler);
signal(SIGUSR2, signal_handler);
// 시그널 대기
while (1)
pause();
return (0);
}
sigaction 버전
void signal_handler(int signum, siginfo_t *info, void *context)
{
(void)context; // 사용하지 않는 매개변수
// 비트 처리 로직은 동일
g_server.current_char <<= 1;
if (signum == SIGUSR2)
g_server.current_char |= 1;
g_server.bit_count++;
if (g_server.bit_count == 8)
{
if (g_server.current_char == '\0')
write(1, "\n", 1);
else
write(1, &g_server.current_char, 1);
g_server.current_char = 0;
g_server.bit_count = 0;
// 클라이언트에게 수신 확인 시그널 전송 (보너스)
kill(info->si_pid, SIGUSR1);
}
}
int main(void)
{
struct sigaction sa;
printf("Server PID: %d\n", getpid());
sa.sa_flags = SA_SIGINFO;
sa.sa_sigaction = signal_handler;
sigemptyset(&sa.sa_mask);
sigaction(SIGUSR1, &sa, NULL);
sigaction(SIGUSR2, &sa, NULL);
while (1)
pause();
return (0);
}
클라이언트 구현
기본 구조
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
void send_char(pid_t server_pid, char c)
{
int bit;
bit = 7; // MSB부터 전송
while (bit >= 0)
{
if ((c >> bit) & 1)
kill(server_pid, SIGUSR2); // 비트가 1
else
kill(server_pid, SIGUSR1); // 비트가 0
usleep(100); // 서버가 처리할 시간 주기
bit--;
}
}
void send_string(pid_t server_pid, char *str)
{
int i;
i = 0;
while (str[i])
{
send_char(server_pid, str[i]);
i++;
}
send_char(server_pid, '\0'); // NULL 문자 전송 (문자열 끝 표시)
}
int main(int argc, char **argv)
{
pid_t server_pid;
if (argc != 3)
{
write(2, "Usage: ./client <server_pid> <message>\n", 40);
return (1);
}
server_pid = atoi(argv[1]);
send_string(server_pid, argv[2]);
return (0);
}
비트 조작 예시
// 문자 'A' (01000001)를 비트별로 전송
char c = 'A'; // 0b01000001
// 7번째 비트 (MSB): 0
if ((c >> 7) & 1) // (0b01000001 >> 7) & 1 = 0b00000000 & 1 = 0
// SIGUSR1 전송
// 6번째 비트: 1
if ((c >> 6) & 1) // (0b01000001 >> 6) & 1 = 0b00000001 & 1 = 1
// SIGUSR2 전송
// ... 계속
보너스
추가 기능:
- 수신 확인 (Acknowledgement)
- 서버가 각 비트를 받을 때마다 클라이언트에게 확인 시그널 전송
- 클라이언트는 확인을 받은 후 다음 비트 전송
- 유니코드 지원
- ASCII 이외의 문자 지원 (UTF-8 등)
수신 확인 구현
서버 (signal_handler 수정):
void signal_handler(int signum, siginfo_t *info, void *context)
{
g_server.current_char <<= 1;
if (signum == SIGUSR2)
g_server.current_char |= 1;
g_server.bit_count++;
// 매 비트마다 수신 확인 전송
kill(info->si_pid, SIGUSR1);
if (g_server.bit_count == 8)
{
if (g_server.current_char == '\0')
{
write(1, "\n", 1);
// 문자열 수신 완료 확인
kill(info->si_pid, SIGUSR2);
}
else
write(1, &g_server.current_char, 1);
g_server.current_char = 0;
g_server.bit_count = 0;
}
}
클라이언트:
volatile sig_atomic_t g_received = 0; // 수신 확인 플래그
void ack_handler(int signum)
{
(void)signum;
g_received = 1;
}
void send_bit(pid_t server_pid, int bit)
{
g_received = 0;
if (bit)
kill(server_pid, SIGUSR2);
else
kill(server_pid, SIGUSR1);
// 수신 확인 대기
while (!g_received)
usleep(10);
}
void send_char(pid_t server_pid, char c)
{
int bit;
bit = 7;
while (bit >= 0)
{
send_bit(server_pid, (c >> bit) & 1);
bit--;
}
}
int main(int argc, char **argv)
{
pid_t server_pid;
if (argc != 3)
return (1);
// 수신 확인 핸들러 등록
signal(SIGUSR1, ack_handler);
server_pid = atoi(argv[1]);
send_string(server_pid, argv[2]);
return (0);
}
주의사항
- 시그널 손실
- 시그널을 너무 빠르게 보내면 일부가 손실될 수 있음
usleep()으로 적절한 지연 시간 부여
- 전역 변수 사용
- 시그널 핸들러에서는 전역 변수 사용이 권장됨
volatile sig_atomic_t타입 사용 권장
- 재진입 가능성 (Reentrancy)
- 시그널 핸들러 내에서 안전한 함수만 사용
write()는 안전하지만printf()는 안전하지 않음
- PID 유효성 검사
- 잘못된 PID로 시그널을 보내면 오류 발생
kill()의 반환값 확인
테스트 예시
# 서버 실행
./server
# 출력: Server PID: 12345
# 다른 터미널에서 클라이언트 실행
./client 12345 "Hello, World!"
# 서버 출력:
# Hello, World!
컴파일 예시
gcc -Wall -Wextra -Werror server.c -o server
gcc -Wall -Wextra -Werror client.c -o client
유용한 함수들
int ft_atoi(const char *str); // 문자열을 정수로 변환
size_t ft_strlen(const char *s); // 문자열 길이
void ft_putstr_fd(char *s, int fd); // 문자열 출력
void ft_putnbr_fd(int n, int fd); // 숫자 출력