100%를 한번에 바꾸는건 어려워도 1%를 100번 바꾸는건 쉽다.

생각정리 자세히보기

42 Seoul

[42 Seoul] minitalk

dc-choi 2022. 6. 3. 15:33
반응형

요약: 이번 프로젝트의 목표는 UNIX 신호를 이용해 데이터를 주고받는 작은 프로그램을 작성해보는 것입니다.

 

알아야할 선수지식

signal()

void (*)(int) signal(int sig, void (*handler)(int));
  • 시그널 처리 방법을 설정한다
  • 몇몇 시그널들은 이미 정의된 행동을 함으로써 처리되는데, 이처럼 기존에 정의된 행동을 그대로 할 지, 시그널을 그냥 무시할지, 아니면 사용자 정의 행동을 하도록 바꿔줄 지 선택할 수 있다
  • sig는 처리해줄 시그널 번호로서, 매크로 SIGABRTSIGALLSIGILLSIGINTSIGFPESIGIOSIGOTHERSIGSEGVSIGTERMSIGUSR1 또는 SIGUSR2 중 하나여야 하며, signal.h 포함 파일에 정의됩니다.
  • *handler는 시그널을 처리해줄 핸들러 SIG_IGN을 인자로 넘겨주면 해당 시그널을 무시한다
  • 함수포인터를 넘겨주면 시그널이 들어왔을 때 특정 함수를 호출한다

 

SIGUSR1

사용자 애플리케이션용입니다. (ANSI로 연장)

 

SIGUSR2

사용자 애플리케이션용입니다. (ANSI로 연장)

 

getpid()

pid_t getpid(void);
  • 현재 프로세스의 아이디를 리턴한다.
  • 아이디는 모든 프로세스가 다 고유한 값을 가지기 때문에 임시 파일명을 지을 때 좋다고 man이 말한다
  • getpid와 getppid는 무조건 성공하기 때문에 (프로그램을 구동한다는 건 반드시 개별적인 프로세스가 동작하는 것과 같으니까 애초에 해당 함수를 불러오는 시점에서 프로세스 아이디는 무조건 존재) 실패했을 경우에 반환하는 값은 X

 

sleep()

unsigned int sleep(unsigned int seconds);
  • seconds 초만큼 스레드를 일시정지한다 (대기 상태). 만약 sleep 중간에 시그널이 발생하면 바로 복귀 후 남은 시간이 반환된다
  • 만약 15초를 쉬는 중에 (seconds = 15) 5초만에 시그널이 발생하여 복귀하였을 경우, 리턴값은 10

 

usleep()

int usleep(useconds_t microseconds);
  • microseconds 마이크로초만큼 스레드를 일시정지한다 (대기 상태)
  • 나머지는 sleep과 동일

 

pause()

int pause(void);
  • kill() 함수나 내부 타이머 (setitimer() 참고) 에 의해 시그널을 받기 전까지 현재 스레드를 일시정지시킨다
  • pause()를 통해 스레드가 일시정지된 상태에서 시그널 처리기 (signal handler) 가 멈출 경우 pause()는 리턴값을 뱉는다고 한다
  • 어떤 경우에서든 무조건 -1을 리턴한다

 

kill()

int kill(pid_t pid, int sig);
  • 무시무시한 이름과는 다르게 죽이는 짓은 안하고 시그널만 전달해주는 얌전한 함수
  • pid에 시그널을 받을 프로세스의 id가 들어간다
    • pid가 0보다 클 경우: 해당 pid에 시그널이 보내짐
    • pid가 0일 경우: 시그널을 보낸 프로세스와 같은 그룹에 속한 (그룹아이디가 같은) 모든 프로세스에 시그널이 보내짐
    • pid가 -1일 경우:
      • 유저가 관리자 권한을 가지고 있다면 (Superuser privilege) 시스템 프로세스와 현재 시그널을 전송하려는 프로세스를 제외한 모든 프로세스에 시그널을 전송
      • 유저가 관리자 권한을 가지고 있지 않다면, 해당 유저와 같은 uid를 가진 프로세스에만 현재 시그널을 전송
  • sig에 해당 프로세스에 보낼 시그널이 들어간다. sig에 0을 넣을 경우 에러 체킹을 시도하며, pid의 유효성을 검사할 수 있다
  • 이때 인자값으로 들어갈 signal은 위에서 말한 매크로에 이미 정의되어 있어야 함
  • 정상적으로 kill 함수가 수행되면 0을 리턴, 에러 발생 시에는 -1 리턴 및 errno 세팅됨

 

Common Instructions

여러분의 프로젝트는 C언어로 작성되어야 합니다.

 

프로젝트는 Norm 규칙에 맞춰 작성되어야 합니다. 보너스 파일/함수가 존재할 경우, 그 또한 norm 검사에 포함하며 norm error가 있을 시 0점을 받게 됩니다.

 

함수들은 정의되지 않은 행동들과 별개로 예기치 않게 중단되어서는 안 됩니다.(예를 들어, segmentation fault, bus error, double free 등) 만약 이렇게 중단되면, 당신의 프로젝트는 작동하지 않는 것으로 여겨지고 평가에서 0점을 받을 것입니다.

 

필요한 경우 heap에 할당된 모든 메모리 공간은 적절하게 해제되어야 합니다. 메모리 누수는 용납될 수 없습니다.

 

과제에서 필요한 경우, `-Wall -Wextra -Werror` 플래그를 지정하여 컴파일을 수행하는 `Makefile`을 제출해야 합니다. Makefile은 relink 되어서는 안 됩니다.

 

`Makefile`은 최소한 `$(NAME), all, clean, fclean, re` 규칙을 포함해야 합니다.

 

프로젝트에 보너스를 제출하려면, Makefile에 `bonus` 규칙을 포함해야 합니다. 이 보너스 규칙은 프로젝트의 메인 파트에서 금지되었던 모든 다양한 헤더, 라이브러리, 또는 함수들을 추가하여야 합니다. 보너스 과제는 반드시 \_`bonus.{c/h}`라는 별도의 파일에 있어야 합니다. 반드시 수행하여야 하는 메인 파트의 평가와 보너스 파트의 평가는 별도로 이뤄집니다.

 

만일 프로젝트에서 여러분의 `libft` 사용을 허용한다면, libft 소스들과 관련 `Makefile`을 함께 루트 폴더 안에 있는 `libft` 폴더에 복사해야 합니다. 프로젝트의 `Makefile`은 우선 `libft`의 `Makefile`을 사용하여 라이브러리를 컴파일한 다음, 프로젝트를 컴파일해야 합니다.

 

이 과제물을 제출할 필요가 없고, 채점 받을 필요가 없을지라도, 저희는 여러분들이 프로젝트를 위한 테스트 프로그램을 만들 것을 권장합니다. 이것은 여러분의 과제물과 동료들의 과제물을 쉽게 테스트할 수 있게 도울 것입니다. 또한, 평가를 진행할 때 이러한 테스트 프로그램들이 특히 유용하다는 사실을 알게 될 것입니다. 평가 시에는 여러분의 테스트 프로그램과 평가 받는 동료의 테스트 프로그램들을 당연히 자유롭게 사용할 수 있습니다.

 

할당된 git 저장소에 과제물을 제출하세요. 오직 git 저장소에 있는 과제물만 등급이 매겨질 것입니다. Deepthought가 평가하는 과제의 경우엔, 동료평가 이후에 Deepthought가 수행됩니다. 만약 Deepthought 평가 중에 오류가 발생한다면, 그 즉시 평가는 중지될 것입니다.

 

실행 파일명은 반드시 `client`와 `server` 이어야 합니다.

 

여러분은 반드시 오류를 세심하게 처리하셔야 합니다. 어떠한 이유 (Segmentation fault, bus error, double free 등) 에서도 프로그램이 예상치 못하게 종료되면 안 됩니다.

 

프로그램에 메모리 누수가 발생하면 안 됩니다.

 

전역 변수는 최대 한 개만 사용 가능하며, 정당한 이유가 있어야 합니다.

 

필수 파트에서는 다음과 같은 함수들을 사용 가능합니다 :

  - `write`
  - `ft_printf` 와 그에 관련된 여러분이 작성한 파일들
  - `signal`
  - `sigemptyset`
  - `sigaddset`
  - `sigaction`
  - `kill`
  - `getpid`
  - `malloc`
  - `free`
  - `pause`
  - `sleep`
  - `usleep`
  - `exit`

 

Mandatory part

여러분은 클라이언트와 서버로 이루어진 통신 프로그램을 작성하여야 합니다.

 

서버가 먼저 실행되어야 하며, 실행된 후에 자기 자신의 PID를 출력하여야 합니다.

 

클라이언트는 다음과 같은 인자값을 받습니다:

- 서버 PID

- 서버로 전송할 문자열

 

클라이언트는 인자로 들어온 문자열을 서버로 전송할 수 있어야 합니다. 서버에서는 문자열을 수신받은 후, 이를 출력하여야 합니다.

 

여러분의 프로그램 끼리의 통신은 반드시 UNIX 시그널을 이용해서만 이루어져야 합니다.

 

서버는 문자열을 최대한 빠르게 출력하여야 합니다. "빠르게" 라 함은, 만약 문자열 수신이 너무 오래 걸리는 것이 느껴진다면 실제로도 꽤 느리게 동작하는 편이라고 생각하시면 됩니다 (힌트를 드리자면, 100글자를 전송하는데 1초가 소요되면 굉장히 느린겁니다)

 

여러분의 서버는 도중에 재시작할 필요 없이, 여러 클라이언트로부터 문자열을 연속적으로 수신받아야 합니다.

 

`SIGUSR1` 과 `SIGUSR2` 단 두 개의 시그널만 사용하여야 합니다.

 

Bonus part

서버는 시그널이 들어올 때마다 클라이언트에 답장 시그널을 보내는 것으로 시그널이 잘 수신되었음을 보장하여야 합니다.

 

유니코드 문자열을 지원해 보세요!

 

구현 설명

// 시그널을 받았을시에 비트 처리하는 함수
void	get_sig(int sig)
{
	// 비트를 저장하는 정적 변수
	static char	temp;
	// 비트를 얼마나 받았는지 확인하는 정적 변수
	static int	bit;
	
	if (sig == SIGUSR1)
	{
 		// temp와 0을 or연산하여 그 값을 temp에 입력
		temp |= 0;
		// 비트가 8이 아닐경우 좌측으로 1만큼 shift
		if (bit < 7)
			temp <<= 1;
	}
	else if (sig == SIGUSR2)
	{
		// temp와 1을 or연산하여 그 값을 temp에 입력
		temp |= 1;
		// 비트가 8이 아닐경우 좌측으로 1만큼 shift
		if (bit < 7)
			temp <<= 1;
	}
	bit++;
	// 비트가 8이 될 경우 저장된 문자를 출력하고 정적변수 초기화
	if (bit == 8)
	{
		write(1, &temp, 1);
		bit = 0;
		temp = 0;
	}
}

int	main(void)
{
	// 서버 실행시 pid 출력하는 로직
	ft_putstr_fd("pid: ", 1);
	ft_putnbr_fd(getpid(), 1);
	ft_putchar_fd('\n', 1);
	// 사용자 지정 시그널을 받으면 처리할 함수 지정해주기
	signal(SIGUSR1, get_sig);
	signal(SIGUSR2, get_sig);
	// 요청이 들어올때까지 계속 대기하기
	while (1)
		pause();
}
void	send_sig(int pid, char *str, int length)
{
	// 문자열의 인덱스
	int	byte_index;
	// 문자의 인덱스
	int	bit_index;
	// 전송할 비트를 나누는 변수
	int	bit_temp;

	byte_index = 0;
	// 들어온 문자열의 길이만큼 루프를 돔
	while (byte_index < length)
	{
		bit_index = 0;
		// 문자열의 문자만큼 루프를 돔
		while (bit_index < 8)
		{
	/*
	str[byte_index] >> (7 - bit_index)만큼 shift 연산한다
	그 값을 1로 and연산해서 비트를 구한다.
	뒤 비트부터 보내야 서버에서 비트를 받아서 문자 출력이 가능함.
	*/
			bit_temp = str[byte_index] >> (7 - bit_index) & 1;
		// bit_temp가 0이면 사용자 지정 시그널 1을 보냄
			if (bit_temp == 0)
				kill(pid, SIGUSR1);
		// bit_temp가 0이면 사용자 지정 시그널 2를 보냄
			else if (bit_temp == 1)
				kill(pid, SIGUSR2);
	// 전송 후 다른 처리를 위해 30 마이크로초만큼 스레드 일시정지
			usleep(30);
			bit_index++;
		}
		// 문자 하나 전송 후 300 마이크로초만큼 스레드 일시정지
		usleep(300);
		byte_index++;
	}
}

// 문자열의 길이를 확인하고 마지막부분에 \n을 추가하는 함수
void	get_str(int pid, char *str)
{
	// 문자열의 길이
	int		length;
	// \n까지 포함한 문자열
	char	*send;
	
	// 문자열 합침
	send = ft_strjoin(str, "\n");
	if (send == NULL)
		exit(1);
	// 문자열의 길이를 구함
	length = ft_strlen(send);
	// 시그널을 보냄
	send_sig(pid, send, length);
	// malloc으로 할당한 문자열 해제
	free(send);
	// 정상 종료
	exit(0);
}

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

	// 매개변수 2개가 들어왔는지 확인    
	if (argc != 3)
	{
		ft_putstr_fd("Argument is Wrong!!\n", 1);
		exit(1);
	}
	// argv는 문자열로 들어오기 때문에 정수로 변환
	pid = ft_atoi(argv[1]);
	// 유효한 pid의 값 범위내에 들어왔는지 확인
	if (pid < 100 || pid > 99998)
	{
		ft_putstr_fd("Invalid PID...\n", 1);
		exit(1);
	}
	// pid와 argv[2]의 문자열을 보냄.
	get_str(pid, argv[2]);
}

 

반응형

'42 Seoul' 카테고리의 다른 글

[42 Seoul] Born2BeRoot 설정가이드(Debian)  (0) 2022.05.13
[42 Seoul] Born2BeRoot 설치가이드(Debian)  (0) 2022.05.12
[42 Seoul] Born2beRoot  (0) 2022.05.09
[42 Seoul] Printf  (0) 2022.04.22
[42 Seoul] Get Next Line  (0) 2022.03.07