본문 바로가기
42seoul/circle-2

[ minitalk ] 구현과정

by saniii 2022. 2. 25.

 

 

두개의 시그널, SIGUSR1와 SIGUSR2만을 이용해서 서버 프로그램에 데이터를 보내야한다. 

사전지식에 대한 이야기는 앞의 게시글에서 가득 했으니 넘어가고 과제를 풀어나가는 생각의 흐름만 기록해보자.

 

+ 두가지의 신호만 사용할 수 있다.

우선 통신하는데 단지 2가지의 시그널만을 이용해야한다는 점을 통해서 이진법을 떠올릴 수 있다.

우리는 C언어 공부를 통해 각 데이터형이 몇바이트로 이루어지는지, 한 바이트는 몇 비트인지를 공부하면서 이진법으로 왠만한 문자열(아스키코드값내의 문자)을 모두 표기할 수 있음을 알고 있다. 

따라서 문자열을 문자로, 문자를 비트로 쪼개어 1(SIGUSR1)과 0(SIGUSR2)로 표현 및, 시그널을 보내면 된다. 

각 문자는 아스키코드값에 따라 숫자로 나타낼 수 있으며 8자리의 이진수로 표현가능하다.( 0 ~ 127 )

따라서 문자열 → 문자  아스키 값(10진수)  2진법  시그널 이용의 과정을 통해 전송하자.

// 10진법 -> 2진법
// 2의 0승 부터 나오니까 비트배열의 뒷자리부터 채워준다. 
 
 while (--i >= 0)
    {
    	bit[i] = msgchar_int % 2;
        msgchar_int = msgchar_int / 2;
    }

 

 

+ 신호를 어떻게 보낼 것인가?

데이터를 어떤 신호로 전송할 것인지는 정했는데 이 신호를 무엇을 통해 전송할 것인가?

 

과제에서 주어진 함수를 공부하면서 kill함수의 기능에 대해 공부했다. 

이름이 kill이라 프로그램을 종료하는데만 사용할 것 같지만 사실 원하는 시그널을 원하는 프로세스에 전달하는 역할을 한다. 

kill함수에 알맞은 시그널을 담아 전송하자.  

kill(server_pid, SIGUSR1);

 

 

+ 서버에서 시그널 SIGUSR1, SIGUSR2의 의미 지정하기

클라이언트의 로직을 짜면서 내가 (혼자서) 임의로 SIGUSR1은 이진법의 1을, SIGUSR2는 이진법의 0을 의미한다고 정했는데, 어떻게하면 서버도 이렇게 생각하도록 할 수 있을까?

 

역시 과제에서 주어진 함수 sigaction을 공부하면 알 수 있다. 

signal()과 sigaction은 비슷한 동작을 하지만 sigaction이 다양한 운영체제에서 더 안정적으로 동작하고, 지정할 수 있는 옵션이 보다 다양하기 때문에 sigaction을 사용한다. 또한 signal()은 시그널을 받고나면 리셋되어 재정의해야한다. 

 

sigaction을 사용하여 SIGUSR1과, SIGUSR2에 대한 동작을 정의한다. 연산을 위해 사용할 변수들을 가장 처음에 초기화해야하는데 이때 그냥 무턱대고 핸들러의 맨앞에서 초기화하기가 애매하다 static 변수들로 적어도 8번은 계속해서 값을 저장해두고 사용해야하는데 초기화장치를 그냥 넣어버리면 연속된 연산에 지장을 준다. 따라서 pid가 지정되었는지 아닌지 확인하여 지금이 변수를 초기화하여야 하는 때인지 아닌지 구분하도록 한다. 

같은 프로세스에서 오는 시그널에 대해 8번동안 연속된 연산을 실행해야한다. (8비트가 한 문자이므로) 따라서 지금 받은 시그널이 앞에 왔던 시그널에 이어져서 오는 시그널인지 확인해줄 로직이 필요하다. 예를 들어 8비트 중 5비트까지 전송되고 다른 프로세스에서 시그널이 왔다면 6번째 비트에 이 시그널을 담을 수 없으므로 지금까지 연산한 값을 초기화시킨다. 

if (g_pid != info->si_pid)
        reset(&bit_length, &message_char, info->si_pid);

 

그리고 SIGUSR1과, SIGUSR2의 가장 중요한 동작, 비트 연산.

message_char = message_char << 1;
if (signum == SIGUSR1)
    message_char = message_char + 1;
else
    message_char = message_char + 0;
bit_length++;
if (bit_length == 8)
{
    itochar = (char)message_char;
    write(1, &itochar, 1);
    reset(&bit_length, &message_char, info->si_pid);
}

우선 2진법에 대해 자릿수 이동을 시키고 시그널에 따라 산술 연산을 지속한다. 로직은 이진수를 십진수로 바꾸는 것을 그대로 적용하되 이진법에서 자릿수 이동을 위해 매번 2를 곱하는 것을 비트 연산자 << 1 로 구현하였다.

 

이어서 8개의 비트를 모아 한 문자열의 아스키값을 완성하면 문자로 출력한 후 사용했던 변수들을 초기화시켰다. 

 

 

 

+ 짧은 문자열 전송은 성공하는데, 긴 문자열은 중간에 씹힌다....?

그간의 슬랙 기록을 찾아보면 평가 중 우리는 만글자 이상의 문자열을 보내야할 수도 있다. 슬랙의 기록을 찾아보지 않더라도 보낼 수 있는 문자열에 한계가 있는 통신 프로그램이라면 매력이 떨어지겠지

음... 쓰다보니 문자열을 시그널과 어떻게 연관지을 것인지는 고민했지만, 문자열을 어떻게 나눠서 보낼 것인지는 별 고민하지 않은 것 같은데       맞다...ㅎ 당연하게 이렇게 로직을 짜기 시작했는데 한글자씩 보내는 것으로 로직을 구성했다. 단순하게 비트를 8개씩 쪼개서(왜냐하면 아스키코드값이 비트 8개로 모두 표현될 수 있기 때문) 시그널을 받으면서 바로 2진법 → 10진법으로 계산하고 시그널을 8번 받으면 그동안 계산한 10진수값을 문자(한글자)로 바꿔 출력하고 다시 0으로 돌아가서 시그널에 대한 진법 계산을 시작한다. 

하지만 다르게 로직을 짠다면 문자 개수 단위로 나눠서 비트를 한번에 받은 다음 '문자열'을 한 번에 뱉는 식으로 짤 수도 있을 것 같다. 

일단 나는 한글자씩 뱉어내는 로직을 선택했다. 

 

잠시 생각이 다른 흐름으로 갔는데 다시 질문에 대해 답해보자면 짧은 문자열을 보내면 그럭저럭 나오는데 만글자 이상의 문자열을 보내면 처음에는 잘 나오다가 중간부터 이상한 글자로 출력된다. (씹힌다.)

이런 현상은 서버에 시그널을 보낸 후 usleep(100);의 텀을 두는 로직을 통해서 해결했다. 중간에 문자열이 씹히는 이유는 kill함수로 시그널을 보내는 속도보다 서버에서 시그널을 받아 처리하는 속도가 더 느리기 때문에 발생한다. 따라서 클라이언트가 서버로 하나의 신호를 보내고 난 뒤 이어서 바로 보내는 것이 아니라 잠시 텀을 줘서 서버가 전송받은 시그널을 처리한 후 새로운 시그널을 받을 수 있도록 한다. 

sleep()함수는 초단위로 이렇게되면 과분한 텀을 주게 되어 프로그램의 효율이 떨어지니 마이크로초 단위로 설정할 수 있는 usleep을 사용하자.

 

 

 

+ 너무 오래걸린다...!

긴 문자열도 모두 잘 나오기는 하는데 만글자는 너무... 오래 걸린다.... 이때까지의 서버의 시그널 처리 로직은 다음과 같다. 

message_char = message_char * 2;
if (signum == SIGUSR1)
    message_char = message_char + 1;
else
    message_char = message_char + 0;

이렇게 (내 머리로 생각할 수 있는 가장 만들어내기 쉬운 로직) 산술 연산을 이용하여 이진법 → 십진법의 로직을 처리하도록 했다. 

 

하지만  산술연산은 로직을 느리게할 수 있다. 따라서 내가 할 수 있는 한에서 산술 연산을 비트 연산으로 바꿔보자. 

이진법을 바꾸는 만큼 비트 연산의 shift를 이용할 수 있다. 비트를 오른쪽으로 n번 움직이는 것은 2의 n승으로 나누는 것과 같고 왼쪽으로 n번 움직이는 것은 2의 n승으로 곱하는 것과 같다.  

    message_char = message_char << 1;
    if (signum == SIGUSR1)
        message_char = message_char + 1;
    else
        message_char = message_char + 0;

별거 안바꾼것 같지만 실행하면 체감할만큼 속도가 빨라진다. 실제로 time 명령어를 이용해서도 확인할 수 있다. 

이제 100자, 1000자는 밀리초로 해결된다. 

 

** 비트연산은 컴퓨터에서 가장 빠르게 실행되는 연산이다. (컴퓨터 입장에서는 재현의 과정을 거치지 않고 즉시 해석하여 바로 동작할 수 있게 때문)

 

 

 

+ 한 클라이언트가 데이터(시그널)를 보내고 있는 와중에 다른 클라이언트가 데이터(시그널)를 보내온다면...?

시그널로 이루어진 통신은 중간에 인터럽트되어도 어디까지 옳게 전송된 건지 알 수 없어 통신에 대한 신뢰도가 없다. 실제로 과제에서도 중간에 클라이언트가 여러명 섞여 데이터가 섞일 수있다고한다. 

signal.h의 다양한 함수를 사용할 수 있다면 시그널을 처리하는 중에 다른 시그널을 블록하여 시그널에 씹히지 않게 할 수 있다. 

 

댓글