여기서 다시 시작하자!


3. sigaction

sigaction은 아래와 같이 이뤄진다.

 

int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);

 

여기서 우리는 struct sigaction라는 구조체를 넘겨주는데 이 구조체를 알아보자.

 

struct sigaction{
	void		(*sa_handler)(int);
        void		(*sa_sigaction)(int, siginfo_t *, void *);
        sigset_t	sa_mask;
        int		sa_flags;
        void		(*sa_restorer)(void);
};

sa_handler

핸들러 함수에 대한 포인터이다. 우리가 signal에서 사용하던 핸들러 함수와 같다.

sa_sigaction

시그널 핸들러를 실행하는 다른 방법인데 추가적인 2개의 인수를 가져간다.

siginfo_t *

받은 시그널에 대한 정보를 제공한다.

sa_mask

시그널이 막힐지를 명시적으로 설정할 수 있도록 허용한다.

sa_flags

핸들링 프로세스의 동작 변경을 허용한다. sa_sigaction 핸들러를 사용하기 위해선 여기에 SA_SIGINFO를 넣어야 한다.

 

자 이제 mask를 안 했을 때의 경우를 보자!

대부분 핸들러를 제공하지 않고 시그널을 막지 않았을 경우 default로 지정된 동작은 프로세스의 종료이다.

SIGSEGV, SIGILL, SIGABRT 류의 시그널은 코어 덤프와 함께 종료한다.

SIGCHLD류의 시그널은 무시된다.

 

우리는 SIGUSR1, SIGUSR2만 사용해야 하는데 이 둘은 default로 지정된 동작은 프로세스의 종료이다.

즉, 우리는 flags에 SA_SIGINFO를 사용해야 한다.

그리고 우리는 sa_mask로 막을 signal은 따로 존재하지 않는다. 어차피 USR1과 USR2 만 사용할 것이기 때문!!!!!!

 

void	handler(int signo, siginfo_t *info, void *context)
{
	if (signo == SIGUSR1)
		write(1, "1", 1);
    else if (signo == SIGUSR2)
		write(1, "0", 1);
}

int		main(void)
{
	struct sigaction act1;

	act1.sa_sigaction = handler;
	act1.sa_flags = SA_SIGINFO;
	if (sigaction(SIGUSR1, &act1, NULL) != 0)
	{
		printf("Sigaction Error");
		exit(1);
	}
	if (sigaction(SIGUSR2, &act1, NULL) != 0)
	{
		printf("Sigaction Error");
		exit(1);
	}
	while (1)
		;
	return (0);
}
void	post(int pid, int idx)
{
	if (idx == 1)
	{
		printf("1");
		kill(pid, SIGUSR1);
	}
	else if (idx == 0)
	{
		printf("0");
		kill(pid, SIGUSR2);
	}
	else
	{
		printf("\n");
		kill(pid, 0);
	}
	usleep(1000);
}

void	conv_and_post(int pid, int c, int num)
{
	if (!c)
	{
		while (num != 8)
		{
			post(pid, 0);
			num++;
		}
		return ;
	}
	else
	{
		conv_and_post(pid, c / 2, ++num);
		post(pid, c % 2);
	}
}

void	post_office(int pid, char *str)
{
	int		c;
	int		num;
	int		idx;

	c = 0;
	idx = -1;
	while (str[++idx])
	{
		num = 0;
		c = (int)str[idx];
		conv_and_post(pid, c, num);
		post(pid, -1);
	}
}

int		main(int argc, char *argv[])
{
	if (argc != 3)
		return (0);
	else
	{
		printf("PID : %s, Massage : %s\n", argv[1], argv[2]);
		post_office(ft_atoi(argv[1]), argv[2]);
	}
	return(0);
}

 

위와 같은 코드 1번째 코드를 실행한 뒤, 2번째 코드에서 PID와 message를 입력 인자로 입력해야 한다.

 

그리고 우리는 글자를 하나하나 보내야 하는데 구분할 수 있는 것은 SIGUSR1과 SIGUSR2 뿐이므로 1, 0

즉, 이진법으로 생각해서 한 글자에 8개의 0과 1의 조합을 가져갈 수 있다. 또한, 아스키코드와 확장 아스키까지 0~255의 수는 2진법으로 바꿔도 8개의 글자를 넘지 않는다.

자! 이게 무슨 말이냐!

글자 하나당 1바이트! 1바이트는 8비트이다.

즉, 우리는 글자를 2진법으로 바꿔서 8비트만큼 쪼개서 보내 뒤 이를 받는 곳에서 다시 8비트를 1바이트로 바꿔서 글자를 복원하면 된다는 뜻이다.

 

두 번째 코드 블록을 보면 signal을 kill 해주고 나서 usleep이 있다.

이 이유를 한 가지 예시로 들면 우리가 밥을 먹을 때 무작정 쉴 틈 없이 입에 집어넣는다고 다 먹는 게 아니지 않은가?

기본적으로 씹고 삼키겠지만 그래도 컴퓨터는 씹는 건 안 하니까 삼킬 때까지의 딜레이를 발생시켜줘야 정상적으로 먹는 것이다.

예시를 벗어나서 시그널을 보내는 갭이 시그널을 받는 갭보다 짧다는 것이다.

 

그럼 이제 글자를 복원하는 작업을 해야 하는데 한 가지 문제가 있다.

0, 1을 받아와서 저장하는 곳이 문제가 된다. 매번 handler에서 선언을 해주면 새것이 되고 그렇다고 받아서 사용하기엔 정해져 있는 틀이 있어서 그러질 못한다.

그래서 전역 변수를 사용해야 하는데 되도록이면 꼭 사용해야 할 곳에만 사용을 해야 하는 것이 규칙이라 조금 애매하다. 다른 방법이 없을까...

void *context

위 부분을 주목해봤지만 이것은 신호 인터럽트 당시 문맥 정보가 포함된 ucontext_t를 가리킨다고 한다.

인터럽트? 이게 무엇인가 하면

interrupt는 마이크로프로세서(CPU)가 프로그램을 실행하고 있을 때, 입출력 하드웨어 등의 장치에 예외상황이 발생하여 처리가 필요할 경우에 마이크로프로세서에게 알려 처리할 수 있도록 하는 것을 말한다. 위키 최고

 

음... 지금 현재 생각나는 방법은 딱히 없어 보인다.

 

전역 변수를 사용하여 생각해보면

우리가 하나씩 받아온 시그널을 전부 다 받고 8개씩 잘라서 복원하는 방법이 있고

시그널이 8개가 차게 되면 1글자를 복원하고 초기화를 해서 다시 받는 방법이 있다.

 

첫 번째 방법은 한 번에 처리를 하는 것에 장점이 있다만 시그널이 끝났다는 것을 어떻게 해야 할지 모른다.

두 번째 방법은 하나하나 처리를 하다 보니 조금 애매한 감이 있지만 시그널이 전부 들어온 이후에도 또 보내서 확인할 수 있다.

 

두 번째 방법으로 해보자!

진행하다 보니 하나의 큰 문제점이 생겼다. 처음부터 피하려고 배제하고 넘겼던 부분 중 하나가 문장이 끝났을 때다.

두 번째 방법으로 진행하니 크게 문제점이 생기는 것이 위의 경우이다. 여러 개의 시그널을 보낸다 하지만 한 덩이의 시그널을 보내고 그 시그널 덩어리가 끝이 났을 때 개행으로 진행을 싶은 욕구가 마구 차오른다. 출력된 보기가 싫어서

 

또한, 우리가 저렇게 보낸 것들의 끝을 안다면 한 번에 출력할 수 있는 상황을 만들어 낼 수 있기 때문에 방법을 찾아서 진행하면 더욱 수월할 것 같다!

 

방법은 마지막에 항상 127을 뜻하는 01111111을 보내주는 방법이다.

자 이렇게 생각했을때 우리는 2진법으로 변환된 시그널을 받고 다시 10진수로 바꾸기보단 비트 연산을 통해 받는다면 더욱 빠르게 연산이 실행될 수 있다.

 

비트 연산에 대한 이야기는 아래 코딩도장에서 너무 잘 다뤄주고 있다.

https://dojang.io/mod/page/view.php?id=175 

 

C 언어 코딩 도장: 23.4 비트 연산 후 할당하기

이번에는 비트 연산자와 할당 연산자를 함께 사용해보겠습니다. a &= b a |= b a ^= b a <<= b a >>= b bitwise_asign.c #include int main() { unsigned char num1 = 4; // 4: 0000 0100 unsigned char num2 = 4; // 4: 0000 0100 unsigned char n

dojang.io

 


4. 구현

 

전체적인 흐름은 이러하다.

server는 실행될 때 본인의 pid를 출력해준다.

이후 client는 server의 pid와 송신할 문자열을 입력 인자로 받는다.

그리고 송신할 문자열들을 ASCII 문자로 바꾸고 다시 2진수로 바꿔서 앞에서부터 server에게 송신을 해준다.

그리고 송신할 문자열이 끝났다면 127을 뜻하는 01111111을 송신하고 문자열이 끝났음을 알린다.

server에서는 수신된 문자열을 받고 count(1~7) 당 해당하는 0 또는 1을 넣어준다.(비트 연산으로)

여기서 만일 수신된 문자열이 99개가 넘어간다면 99개의 문자열을 출력해준다.

그리고 다시 수신을 받는다.

위의 상황이 아니라면 127을 뜻하는 문자를 받았을 때 끝까지 출력한 후 개행을 출력해주고 처음의 상태로 돌아간다.

코드의 로직은 아래와 같다.

server.c

void	*reset(void *b, size_t len)
{
	unsigned char	*temp;
	unsigned long	i;

	temp = (unsigned char*)b;
	i = -1;
	while (++i < len)
		temp[i] &= 0;
	return (temp);
}

void	handler(int signo, siginfo_t *info, void *context)
{
	static unsigned char	buf[100];
	static int				idx;
	static int				count;

	(void)info;
	(void)context;
	if (--count == -1)
	{
		count = 7;
		idx++;
	}
	buf[idx] &= ~128;
	if (signo == SIGUSR1)
		buf[idx] |= (1 << count);
	else if (signo == SIGUSR2)
		buf[idx] &= ~(1 << count);
	if (buf[idx] == 127 || idx == 99)
	{
		write(1, buf, idx + 1);
		if (buf[idx] == 127)
			write(1, "\n", 1);
		reset(buf, 100);
		idx = 0;
	}
}

int		display_pid()
{
	char *pid;

	if (!(pid = ft_itoa(getpid())))
		return (0);
	write(1, "My PID is ", 10);
	write(1, pid, ft_strlen(pid));
	write(1, "\n", 1);
	free(pid);
	return (1);
}

int		main(void)
{
	struct sigaction	act1;

	act1.sa_sigaction = handler;
	act1.sa_flags = SA_SIGINFO;
	if (!(display_pid()))
	{
		write(1, "PID malloc error", 16);
		exit(1);
	}
	if (sigaction(SIGUSR1, &act1, NULL) != 0)
	{
		write(1, "Sigaction Error", 15);
		exit(1);
	}
	if (sigaction(SIGUSR2, &act1, NULL) != 0)
	{
		write(1, "Sigaction Error", 15);
		exit(1);
	}
	while (1)
		;
	return (0);
}

client.c

void	post(int pid, int idx)
{
	if (idx == 1)
		kill(pid, SIGUSR1);
	else if (idx == 0)
		kill(pid, SIGUSR2);
	usleep(100);
}

void	conv_and_post(int pid, int c, int num)
{
	if (!c)
	{
		while (num < 8)
		{
			post(pid, 0);
			num++;
		}
		return ;
	}
	else
	{
		conv_and_post(pid, c / 2, ++num);
		post(pid, c % 2);
	}
}

void	post_office(int pid, char *str)
{
	int		c;
	int		num;
	int		idx;
	int		last;

	c = 0;
	idx = -1;
	last = 7;
	while (str[++idx])
	{
		num = 0;
		c = (int)str[idx];
		conv_and_post(pid, c, num);
	}
	post(pid, 0);
	while (last--)
		post(pid, 1);
}

int		main(int argc, char *argv[])
{
	if (argc != 3)
		return (0);
	else
	{
		write(1, "Client PID : ", 13);
		write(1, argv[1], ft_strlen(argv[1]));
		write(1, "\nMassage : ", 11);
		write(1, argv[2], ft_strlen(argv[2]));
		write(1, "\n", 1);
		post_office(ft_atoi(argv[1]), argv[2]);
	}
	return (0);
}

 


*. Bonus

위 코드에서 보너스를 구현하려 한다.

보너스는 두 가지 상황을 더 준다.

첫 번째 수신이 잘 되었는지 확인하는 시스템을 만든다.

두 번째 유니코드 문자도 출력하게 한다.

 

첫 번째 문제는 다음과 같이 생각했다.

우선 client는 server에게 client의 pid를 송신해주어야 한다.

왜냐하면 수신이 잘 되었으면 1이라는 신호를 받고 종료를 시키게 한다면 "done!"과 같은 문자열을 출력해줄 수 있기에 이를 수신이 잘 되었다는 시스템을 확인할 수 있게 된다.

또한, client의 pid는 언제 송신해야 하는가? 이는 127을 수신하기 전에 하면 된다.

이 또한, 수신이 잘 돼야만 들어올 수 있기 때문에 127을 받기 전에 해야 한다고 생각했다.

즉, '송신할 문자열 -> pid -> 127'이 순서가 된다.

이렇게 하면 server는 pid를 받고 127을 받으면 그 pid를 통해 SIGUSR1을 송신하게 된다.

이후 client는 SIGUSR1을 받고 수신이 잘 되었구나 생각한 뒤 끝나게 하면 된다.

여기서 한 가지 문제가 있다. 수신을 "잘 받았구나!"라고 생각했을 땐 확인이 되지만 수신이 정상적이지 않다면 어떻게 해야 할까?

 

두 번째 문제는 사실 UNICODE에 대해 생각해봐야 한다.

우리는 UNICODE가 어떻게 이뤄지는지를 먼저 알아야 한다.

여기서 우리는 UTF-8, UTF-16 UTF-32 등을 본 적이 있을 것이다.

이를 쉽게 생각해서 'UTF-숫자'에서 숫자는 비트를 생각하면 된다. 즉, 1 글자당 32비트를 사용하여 프린트되게 하는 것이다.

근데 생각해보면 1글자당 32비트라면 문자열이 엄청나게 크게 전송이 될 것이다. 1글자당 2진수 32개의 문자열을 갖는 것이니까 말이다.

그래서 UTF-16이 나왔다. 하지만  UTF-16은 이런 문제가 있다. ASCII 문자와 호환 문제가 있다. 최소 2바이트를 사용하기 때문이다.

또한 2바이트를 사용해 문자를 표현하기에 엔디안 문제가 발생한다.

엔디안은 쉽게 말해 한국의 책은 왼쪽이 첫 페이지로 오른쪽으로 진행한다. 하지만, 일본의 책은 오른쪽이 첫페이지로 왼쪽으로 진행한다.

즉, 순서를 이야기하는 것이다. 큰 것을 첫 번째에 두느냐 마지막에 두느냐에 따라 엔디안이 빅이냐 리틀이냐로 나뉜다.

다시 돌아가서! 그래서 UTF-8이 나타났다. 이는 1~4바이트로 인코딩 될 수 있으며 ASCII 문자와 호환이 된다.

즉, 이는 ASCII 문자의 조합으로 영어 이외에 문자를 출력하는 것이다.

이 조합은 아래와 같이 나타낸다.

U+0000~U+007F : UTF-8 Encoding 0xxx xxxx 8bit (총 1byte)

U+0080~U+07FF : UTF-8 Encoding 110x xxxx 10xx xxxx 16bit (총 2byte)

U+0800~U+7FFF : UTF-8 Encoding 1110 xxxx 110x xxxx 10xx xxxx 24bit (총 3byte)

U+10000~U+10FFFF : UTF-8 Encoding 1111 0xxx 10xx xxxx 10xx xxxx 10xx xxxx 32bit (총 4byte)

 

자 이제 생각해보자!

우리가 어떻게 유니코드를 저런 방식으로 출력을 할 수 있을까?

 

우리는 유니코드에서 8bit는 7개, 16bit에서는 11개, 24bit에서는 15개, 32bit에서는 21개의 신호를 융합해서 보내야 한다.

즉, 우리가 생각했을 때 8bit는 그냥 ASCII 문자로 출력을 하되 만일 c가 128을 넘어간다면 유니코드로 넘기는 것으로 생각해보자.

왜냐하면 1000 0000은 128이기 때문이다.

 

우리가 생각했을 때 범위를 계산한다면

16bit는 $2^8$ ~ $2^{12} - 1$

24bit는 $2^{12}$ ~ $2^{16} - 1$

32bit는 $2^{16}$ ~ $2^{22} - 1$

이렇게 생각해서 분배한 뒤 2진수로 변환한 뒤 각 양식에 맞춰서 보내주면 된다!


결론!

 

유니코드 처리하는 방법은 만약에 유니코드 중 ☠ 이런 문양을 보여주고 싶다면

☠ 는 9760의 유니코드를 갖고 있는데 이것은 226 | 152 | 160 와도 같다.

무슨 말이냐 하면 9760을 이진수로 변환한다면 1110 0010 1001 1000 1010 0000으로 나온다!

즉, 1110 0010 | 1001 1000 | 1010 0000 = 226 | 152 | 160

이것을 총 3개의 char로 받아서 출력하니까 이것을 하나씩 받아서 출력하면서 끊기지 않으면 된다.

예를 들어 우리가 98번째에 ☠를 출력하고 싶은데 그렇다면 226에서 끊기니 그전에 출력하고 이후로 들어가야 한다는 말이다.

우리는 그럼 이 문제를 처리하기 위해 어떻게 해야 하는가????

우리는 시그널을 받아 저장한 문자열이 90번째가 넘고 그때의 문자가 192(1100 0000)를 넘어가면 그때를 temp로 저장하고 이전까지만 출력한 뒤, 문자열의 첫 번째를 temp로 선언해주면 이후 결과 출력이 가능하다!

송수신 관계는 받았다면 받은 것을 확인만 하는 것으로 결과를 내고 끝냈다!

'42일기' 카테고리의 다른 글

python Dict, set 활용  (0) 2021.10.15
Philosophers  (2) 2021.07.09
Push_swap  (0) 2021.06.24
Minitalk(1)  (4) 2021.06.13
Fract-ol  (6) 2021.06.08

+ Recent posts