minitalk과제는 posix 시스템의 신호를 이용해서 서로 다른 프로세스 간(클라이언트에서 서버로 문자열을 전달해야 한다.) 데이터를 전달하는 것을 구현하는 과제다. 이 때 허용되는 신호는 SIGUSR1, SIGUSR2 단 두가지 뿐이다. 즉, 해당 과제는 0과 1로만 통신하여 데이터를 전송하는 것으로 여겨야한다.
1. 데이터 전송 과정
1) SIGNAL - posix
posix 시스템에서 서로 다른 프로세스 끼리 통신할 수 있는 방법인 signal은 소프트웨어 인터럽트를 이용하여 다른 프로세스에게 간단한 값을 보낼 수 있는 방법이다. 그 값의 예로 ctrl + c는 SIGINT(터미널에서 프로세스에게 인터럽트)의 값을 보내고 ctrl + z는 SIGTSTP(프로세스를 임시 중지)를 전송한다. 다음은 신호의 값에 따른 의미를 정리한 표이다.
Signal | Default Action | Description |
SIGABRT | A | 프로세스 중단 |
SIGALRM | T | 알람 신호 - alram 함수에서 사용 (real time) |
SIGBUS | A | 정의되지 않은 메모리 객체에 접근 |
SIGCHLD | I | 자식 프로세스 종료, 중단 혹은 계속 |
SIGCONT | C | 정지하지 않으면 계속 실행 |
SIGFPE | A | 산술적 오류 ex) 0으로 나눗셈, 정수형 자료형에 float연산 |
SIGHUP | T | Hangup- ex) 터미널에서 접속이 끊어졌을을 때 |
SIGILL | A | Illegal instruction. |
SIGINT | T | 터미널 인터럽트 신호 |
SIGKILL | T | 프로세스 강제 종료 (무시되거나 행동을 변경할 수 없음) |
SIGPIPE | T | 사용자가 없는 파이프를 사용 ex) 이미 닫힌 소켓에 데이터 전송 |
SIGQUIT | A | 터미널 종료 신호 |
SIGSEGV | A | 잘못된 메모리 참조 |
SIGSTOP | S | 프로세스 강제 정지 신호 (무시되거나 행동을 변경할 수 없음) |
SIGTERM | T | 종료 신호 (kill과 달리 무시, 또는 다른 행동으로 처리 될 수 있음) |
SIGTSTP | S | 프로세스 정지 |
SIGTTIN | S | 백그라운드 프로세스 읽기 시도 |
SIGTTOU | S | 백그라운드 프로세스 쓰기 시도 |
SIGUSR1 | T | User-defined signal 1. |
SIGUSR2 | T | User-defined signal 2. |
SIGPOLL | T | Pollable event - ex) 소켓에 데이터가 전송이 완료되었는지 (특정 이벤트가 발생했는지) |
SIGPROF | T | 프로파일링 타이머 신호 (sigalrm과 달리 현재 프로세스와 커널에서 실행된 cpu시간을 비교) |
SIGSYS | A | 시스템 호출이 실패했을 때 발생 |
SIGTRAP | A | Trace/breakpoint trap. (일반적으로 디버깅 모드에서 사용) |
SIGURG | I | out of band 데이터 도착 (대용량 데이터가 소켓에 도착하여 처리가 필요할 때) |
SIGVTALRM | T | 알람 신호 (cpu time 측정) |
SIGXCPU | A | cpu 시간 제한 초과 |
SIGXFSZ | A | 파일 크기 제한 초과 |
T : 프로세스 종료
A : 프로세스 종료, core 파일(종료 직전 메모리 상황을 저장) 생성과 같은 지정된 행동이 동반될 수 있음
I : 신호 무시
S : 프로세스 정지
C : 프로세스가 정지해 있다면 다시 실행, 그 외에는 무시
signal은 한 프로세스에 여러개의 같은 신호를 보낸다면 프로세스 핸들링 함수에서 처리가 끝나기 전까지 같은 신호를 모두 무시한다. (신호의 정보를 담은 siginfo_t가 overwritten 되지 않기 위함)
signal을 한 프로세스에 여러개의 다른 신호를 보낸다면 queue에 저장되며 (이를 pending이라 한다) 어떤 순서로 보내질지는 정해지지 않는다.
위에 서술한 signal의 특성 덕분에 usleep()없이 minitalk를 구현한다면 출력이 안되거나 문자열이 깨진 채로 출력되는 것을 볼 수 있었다. 이는 server에서 신호를 처리하기도 전에 client가 신호를 계속 보내서 신호의 순서가 바뀌거나 같은 신호가 무시되었기 때문이다. 이를 해결하는 가장 간단한 방법은 server에서 신호를 처리하기 충분한 시간을 두며 신호를 보내는 것이다. 그러나 이 방법은 server에 신호가 도착했는지 불확실하다. 따라서 usleep을 사용하는 대신 보너스 요구사항인 ack를 구현하기로 마음먹었다.
2. ack 구현
1) ack 란?
ack는 tcp에서 신뢰성 있는 패킷 전달을 위해 고안된 방법이다.
어려운 문제는 한번에 풀 수 없다. 따라서 저수준(물리)에서 고수준(어플리케이션)까지 코드를 쌓아 올린 것을 계층이라 하며 네트워크에서는 osi 7 혹은 tcp/ip 4 계층이라 부른다. osi 7계층에는 물리 - 데이터 링크 - 네트워크 - 전송 - 세션 - 표현 - 응용 계층으로 나뉘는데 tcp는 4계층인 전송 계층에 속한다.
TCP 프로토콜
UDP 프로토콜
ack는 acknowledgement의 약자로 데이터 송신측이 데이터 수신측으로 성공적으로 받았는지를 알 수 있는 방법이다. 이 때 수신자가 성공적으로 데이터를 받았다면 송신자에게 다음으로 받아야할 데이터의 번호를 ack로 보내고 수신자가 이를 받아서 다음 데이터를 전송한다. 만약 송신자에게 ack가 돌아오지 않았거나 nack로 문제가 있는 번호를 보낸다면 해당 데이터를 다시 전송한다.
이를 그림으로 그린다면 다음과 같다.
위와 같이 1개의 패킷을 받고 ack를 기다린다면 stop & wait라고 부른다. 이 때 기다리는 시간을 time out이라 부르며, 해당 시간까지 ack가 오지 않거나 nack이 오면 문제가 생겼다 판단하고 데이터를 재전송한다.
반면 아래와 같이 한번에 여러개의 패킷을 받고 그에 따른 ack를 여러개를 보내면 sliding window라고 부른다(tcp 패킷의 window size와 관련있다) 다음은 window의 크기가 4일 때의 모습이다.
sliding window에서는 중간에 ack를 못 받았거나 nack를 받았을 때 대표적으로 두가지 방법으로 패킷을 재전송한다.
•
go back n : 문제가 생긴 패킷부터 전부 다시 재전송한다. 이 때 수신측에서는 잘못된 패킷 이후에 온 패킷(8 ~ 10)은 ack를 보내지 않는다.
•
selective repeat : 문제가 생긴 패킷만 따로 재전송한다. (7번 패킷만 재전송)
2) 패킷 설계 및 구현
글쓴이의 구현 방법에서는 server가 신호를 받았을 때 처리하는 함수가 client가 신호를 보내는데 실행하는 함수보다 실행 내용이 많았다. 따라서 한번에 여러개의 신호를 보내도 server에서 처리할 수 없었기에 stop & wait를 사용했다. 또한 본래 ack는 다음 패킷의 번호를 전송해야 하지만 신호를 많이 보낼 수록 오류 확률이 늘어나기 때문에 SIGUSR1을 ack로 SIGUSR2를 nack로 가정했다.
다음은 패킷의 구조를 간단히 표현한 표다.
바이트 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
seq num | data | data | data | data | data | data | data | data | checksum |
패킷의 크기는 사용할 환경에 최적화 된 크기를 가져야 한다. 클러스터 맥 환경에서는 1 비트마다 ack를 사용할 때 80bit를 연속으로 전송 할 수 있었고 그 이상일 경우 신호가 전달되지 않는 현상이 발생했다. 따라서 패킷은 10바이트로 제한했다.
또한 패킷의 크기는 고정되어있고 ack번호도 필요 없기에 패킷에는 시퀀스 번호와 체크섬만 포함하였다. 체크섬은 구현에 따라 충돌 가능성이 정해지는데 해당 구현에서는 복잡한 검증이 필요하지 않기 때문에 패킷의 1 ~ 9 바이트 까지의 1인 비트의 개수를 계산하였다.
이 때 체크섬은 0 ~ 72 까지의 값을 가지게 된다. 구현 상으로는 ack가 돌아오지 않을경우 nack(체크섬이 잘못되었을 경우 전송)을 받을 때 까지 1을 서버에 전달하기에 데이터 전송 중 실패하면 체크섬은 256이 되어 오류로 탐지 가능하다.
전송 오류는 3가지 상황이 발생할 수 있다.
•
시퀀스 번호 전달 도중 실패
checksum 까지 전부 1비트로 설정되어 유효하지 않은 256으로 체크섬이 설정된다.
•
데이터 전달 도중 실패
checksum 까지 전부 1비트로 설정되어 유효하지 않은 256으로 체크섬이 설정된다.
•
체크섬 전달 도중 실패
만약 checksum이 운이 좋게 유효할 경우(00001111일 때 6번째 에서 실패하면 client에서는 1만 보내기 때문에 성공적으로 전달받은 것과 동일해진다) 해당 패킷을 그대로 사용가능하기에 ack를 전송한다.
그러나 client는 nack를 받을 때까지 1을 보내기 때문에 다음 패킷이 유효하지 않아(전부 1로 이루어진 패킷이 전송되어 버린다) nack을 보낸 후 client는 중복된 패킷부터 다시 전송한다.
sequence number | server | client |
1 | ack | ack |
2 | checksum = ok → ack | time out → until recieve nack send 1 |
유효하지 않은 번호 | 11111111……11111111 → nack | until recieve nack send 1 |
2 | ack | ack |