Minitalk

이동윤·2025년 2월 17일
0

[프로젝트 지침]

✅ 1. 실행 파일 이름

클라이언트: client
서버: server

→ 이름을 반드시 위와 같이 지정해야 하며, 다른 이름으로 제출하면 안 됩니다.

✅ 2. Makefile 규칙

프로젝트는 Makefile로 컴파일해야 합니다.
Makefile은 relink(불필요한 재컴파일)를 해서는 안 됩니다.
→ make를 여러 번 실행해도 실행 파일이 재생성되면 안 됨.

✅ 3. libft 사용

libft 사용이 허용됩니다.
libft의 소스 코드와 Makefile을 프로젝트에 포함해야 하며, Minitalk의 Makefile에서 libft를 먼저 빌드한 후, 전체 프로젝트를 컴파일해야 합니다.

✅ 4. 오류 처리

모든 오류를 철저하게 처리해야 합니다.
프로그램이 비정상 종료하면 안 됩니다. 예시:
Segmentation fault (잘못된 메모리 접근)
Bus error (잘못된 메모리 정렬)
Double free (동일한 메모리 두 번 해제)
잘못된 인자 처리
오류 상황에서 반드시 적절한 에러 메시지를 출력하고 프로그램을 종료해야 합니다.

✅ 5. 메모리 누수 금지

malloc으로 할당한 메모리는 반드시 free로 해제해야 합니다.
메모리 누수(memory leak)가 있으면 감점 또는 0점 처리됩니다.
→ 메모리 누수를 확인하려면 valgrind를 적극적으로 사용하세요.
bash
Copy
Edit
valgrind --leak-check=full --show-leak-kinds=all ./server
✅ 6. 전역 변수 제한
각 프로그램(client, server) 당 하나의 전역 변수만 사용할 수 있습니다.
전역 변수를 사용할 경우 그 이유를 명확하게 설명해야 합니다.
→ 예: 시그널 핸들러에서 상태를 유지하거나 데이터를 전달할 때 필요


필수 구현 사항 (Mandatory Part)

이제 본격적으로 Minitalk의 필수 구현 요구 사항을 살펴보겠습니다.
핵심 개념은 UNIX 시그널(SIGUSR1, SIGUSR2)을 이용한 서버-클라이언트 통신입니다.

✅ 1. 프로그램 개요

Minitalk은 클라이언트-서버 기반 통신 프로그램입니다.

  • 서버(server)

    • 먼저 실행되어야 함
    • 실행 직후 자신의 PID(프로세스 ID)를 출력
    • 클라이언트로부터 문자열을 받아 출력
  • 클라이언트(client)

    • 실행 시 두 개의 인자를 받음
    • 서버의 PID
    • 전송할 문자열
    • 해당 문자열을 서버로 UNIX 시그널을 이용해 전송
    • 서버가 문자열을 정상적으로 출력하면 성공

✅ 2. 출력 속도 (성능 요구 사항)

서버는 문자열을 즉시 출력해야 함
출력이 너무 느리면 감점
예: 100글자 출력에 1초 걸리면 너무 느림 → 감점 요소!
너무 많은 sleep(), usleep() 사용 금지
적절한 속도로 SIGUSR1 / SIGUSR2를 활용하여 데이터 전송

✅ 3. 서버는 여러 클라이언트의 요청을 연속해서 처리 가능해야 함

서버를 재시작하지 않고 계속 실행 가능해야 함.
여러 클라이언트가 순차적으로 메시지를 보낼 수 있어야 함.

✅ 4. 통신 방식 (UNIX 시그널 활용)

클라이언트 → 서버 데이터 전송
SIGUSR1(사용자 정의 시그널 1)
SIGUSR2(사용자 정의 시그널 2)
단, 리눅스에서는 동일한 시그널이 동시에 여러 개 도착하면 큐잉(저장)되지 않음!
즉, 서버가 아직 이전 시그널을 처리하지 않았는데 추가로 시그널이 오면 일부 손실 가능
이 문제를 해결하려면 클라이언트가 서버의 응답을 기다리는 방식을 고려해야 함


signal이란?

유닉스(Unix)에서 Signal은 프로세스(운영 체제에서 실행 중인 프로그램)간 통신(IPC, Inter-Process Communication)의 한 형태로, 프로세스나 커널이 다른 프로세스에 특정한 알림이나 요청을 보내는 메커니즘입니다. Signal은 보통 프로세스에 특정 행동을 하도록 유도하거나, 프로세스를 제어하기 위해 사용됩니다. 예를 들어, 프로세스를 종료시키거나, 일시 중지하거나, 프로세스가 특정 작업을 완료하도록 요청하는 등의 용도로 사용됩니다.

  1. Signal의 기본 개념
    Signal은 일종의 인터럽트로 볼 수 있습니다. 하나의 프로세스가 다른 프로세스에 특정 알림을 전달하거나 명령을 내리는 방식입니다. Signal은 하드웨어 인터럽트와 유사하지만, 프로세스 간의 소프트웨어적인 통신입니다.

  2. Signal의 종류
    유닉스 시스템에는 다양한 종류의 Signal이 존재합니다. 각 Signal은 특정한 행동을 프로세스에 요구하며, Signal을 받은 프로세스는 그에 맞는 행동을 합니다.

📌 1. SIGUSR1, SIGUSR2란?

SIGUSR1과 SIGUSR2는 리눅스/유닉스 시스템에서 미리 정의된 시그널
사용자가 자유롭게 활용 가능한 시그널 (User-defined Signal)
다른 시그널(SIGKILL, SIGINT 등)과 달리, 특정한 기본 동작이 없음
→ 내가 원하는 대로 핸들링해서 사용할 수 있음!

✅ 그렇다면 어디에서 정의되어 있을까?

#include <signal.h>

<signal.h> 헤더 파일 안에 미리 정의된 매크로로 존재함.
즉, 네가 따로 #define SIGUSR1 10 이런 걸 선언할 필요 없이, 이미 있는 값이다.

💡 운영체제마다 시그널 번호는 다를 수 있음!

리눅스에서는 보통:
SIGUSR1 = 10
SIGUSR2 = 12
kill -l 명령어로 시스템에서 정의된 시그널 목록을 확인할 수 있음.

📌 2. 왜 SIGUSR1, SIGUSR2가 필요할까?

운영체제가 기본적으로 제공하는 시그널(예: SIGKILL, SIGINT)은 특정한 의미가 정해져 있음.
예를 들어:

SIGKILL (9) → 무조건 프로세스를 강제 종료
SIGINT (2) → Ctrl + C 입력 시 종료
하지만, 사용자가 특정 기능을 수행하는 시그널이 필요할 수도 있음!

즉, 일종의 빈 시그널이라고 할 수 있는 SIGUSR1, SIGUSR2를 제공해 주는 것이다~


pid란?

PID는 Process Identifier의 약자로, 운영 체제에서 각 프로세스를 고유하게 식별하기 위해 부여되는 숫자입니다. 각 프로세스는 시스템 내에서 유일한 ID를 가지고 있어야 합니다. 이 PID는 시스템의 프로세스를 관리하고 추적하는 데 중요한 역할을 합니다.

1. PID의 역할
PID는 운영 체제의 프로세스 관리 시스템에서 중요한 역할을 합니다. 시스템에서 여러 개의 프로세스가 동시에 실행되고 있을 때, PID는 각 프로세스를 구별할 수 있게 해줍니다. 예를 들어, 프로세스를 종료시키거나 리소스를 할당할 때, 특정 PID를 지정하여 어떤 프로세스에 영향을 줄지 명확히 알 수 있습니다.

2. PID의 사용
프로세스 제어: 운영 체제는 PID를 사용하여 프로세스를 시작하고, 종료하고, 일시 중지하거나 다시 시작하는 등의 작업을 수행합니다.
리소스 할당: 메모리, CPU 시간 등의 자원도 PID를 기준으로 할당됩니다.
디버깅 및 모니터링: 프로세스의 상태를 추적하고, 어떤 프로세스가 어떤 자원을 사용하는지 확인하는 데도 PID가 사용됩니다.

3. 자료형
PID는 정수형으로 표현됩니다. 대부분의 운영 체제에서 PID는 int 타입의 변수로 저장됩니다. 그러나 PID의 범위는 운영 체제와 시스템에 따라 달라질 수 있습니다. 일반적으로 PID는 양의 정수로 표현되며, 보통 0부터 시작해서 시스템에서 관리할 수 있는 최대값까지 할당됩니다.

예시:
Linux나 Unix 계열 시스템에서는 PID가 보통 32비트 또는 64비트 정수로 표현됩니다.
윈도우에서는 DWORD (32비트 unsigned integer)로 PID를 다루기도 합니다.
4. PID의 범위
PID의 범위는 시스템에 따라 다르지만, 일반적으로 운영 체제의 설정에 따라 제한됩니다. 예를 들어, Linux에서는 PID의 기본 범위가 1부터 32,768 (혹은 4억 이상의 큰 범위로 설정 가능)까지일 수 있습니다. 그러나 시스템 자원의 한계나 설정에 따라 범위는 다를 수 있습니다.

  1. PID와 0번 프로세스 (PID 0)
    PID 0: 이 번호는 특별한 경우로 사용됩니다. PID 0은 커널 프로세스를 나타내며, 실제로 사용자 프로세스와는 다릅니다. 이는 운영 체제의 핵심적인 기능을 담당하는 프로세스들로, 사용자가 직접 종료하거나 변경할 수 없습니다.

signal 함수란?

1️⃣ signal() 함수란?

signal() 함수는 특정 시그널(Signal)이 발생했을 때 실행할 동작(핸들러)을 설정하는 함수이다.
즉, 프로세스가 특정 시그널을 받으면 어떻게 반응할지 정의하는 역할을 한다.

📌 시그널(Signal) 이란?

운영 체제가 프로세스에게 보내는 이벤트 알림입니다.
예를 들어, Ctrl + C(SIGINT) 입력 시 프로그램이 종료되는 것도 시그널의 예입니다.

2️⃣ signal() 함수 원형

#include <signal.h>

void (*signal(int signum, void (*handler)(int)))(int);

📌 매개변수

int signum: 처리할 시그널 번호 (SIGINT, SIGTERM, SIGKILL 등)
void (*handler)(int): 해당 시그널이 발생했을 때 실행할 핸들러 함수

📌 반환값

성공하면 이전 핸들러(기존에 등록된 핸들러 함수) 반환
실패하면 SIG_ERR 반환

3️⃣ signal()의 동작 방식

✅ 특정 시그널이 발생하면, signal()에 등록된 핸들러 함수가 실행됨.

✅ 시그널을 처리하는 방법은 3가지:

  • 사용자 정의 핸들러 등록 (가장 기본적인 방식)
  • 시그널 무시 (SIG_IGN)
  • 기본 동작 복원 (SIG_DFL)

server.c

#include "minitalk.h"

void	rec_putnbr(int nb, int fd)
{
	char	c;

	if (nb == 0)
		return ;
	c = '0' + nb % 10;
	rec_putnbr(nb / 10, fd);
	write(fd, &c, 1);
}

void	ft_putnbr_fd(int n, int fd)
{
	char	c;

	if (n < 0)
	{
		write(fd, "-", 1);
		c = '0' - n % 10;
		rec_putnbr(-(n / 10), fd);
	}
	else
	{
		c = '0' + n % 10;
		rec_putnbr(n / 10, fd);
	}
	write(fd, &c, 1);
}

static void	ft_handler(int signal)
{
	static int	bit; // 비트를 얼마나 받았는 지 확인하는 정적 변수
	static char	tmp; // 문자값을 저장하는 정적 변수

	if (signal == SIGUSR1) // SIGUSR2 일 때는 어차피 0이므로 pass 가능
		tmp |= (1 << bit); // 가장 오른쪽 비트부터 추가해줌
	bit++;
	if (bit == 8) // 비트가 8이 될 경우 저장된 문자를 출력하고 정적변수 초기화
	{
		write(1, &tmp,1);
		bit = 0;
		tmp = 0;
	}
}

int	main(int argc, char **argv)
{
	pid_t	pid;

	(void)argv;
	if (argc != 1)
	{
		write(1, "Error", 5);
		return (0);
	}
	pid = getpid();
	write(1, "PID :", 5);
	ft_putnbr_fd(pid, 1);
	write(1, "\n",1);
    // SIGUSR1신호와 SIGUSR2 신호 입력시 ft_handler함수 호출
	signal(SIGUSR1, ft_handler);
	signal(SIGUSR2, ft_handler);
	while (argc == 1)
		pause(); // 신호가 입력될 때 까지 대기
	return (0);
}

0개의 댓글