1. Introduction
Serial Port 통신 프로그램을 만들면서 애를 많이 먹었던 이유는 직렬 포트의 버퍼를 그대로 내 프로그램의 버퍼로 이용하려고 했기 때문인것 같다. 하지만 나는 아직도  굳이 프로그램에서 버퍼를 구현할 필요가 있나 싶다. 어쨌든, 내 프로그램에서 버퍼를 구현함으로써 직렬 포트 통신을 가능하게 했으니 이젠 그 문제는 나를 신경쓰이게 하지 않는다...



2. Revisiting the Problem
애초에 내가 고민했던 thread를 사용하게 됨에 따라 생기게 될 문제는 사실 문제가 아니었다. 처음에 가지고 있던 고민은 내가 직렬 포트에 연결된 device에 어떤 request를 보내고 그것에 대한 response를 받아야 할 경우에 request에 대한 정보가 아닌 연결된 device에서 보낸 독립적인 정보이면 이 정보와 애초에 request에 대한 response를 어떻게 분간하는가에 대한 고민이었다. 하지만 이것이 크게 문제가 되지 않는 이유는 특정한 상황이 아니면 직렬 포트에 연결된 device에서 request와 독립적인 어떻한 정보도 보내지 않기 때문이다. 이 시나리오의 발생 가능성이 전혀 없는건 아니지만, 발생해서 문제가 생긴다 하여도 이것은 프로토콜의 문제라고 생각한다. 그리고 모든 요청은 직렬 포트를 타고 순차적으로 이루어지고 요청에 대한 응답은 순차적으로 오기 때문에 여러 요청에 대한 응답을 구분하는것도 큰 문제가 되지 않음을 깨달았다. 직렬 포트 통신 구현을 위해 가장 핵심적인것은 직렬 포트에서 수신된 데이터를 얼마나 자유자재로 읽어올 수 있느냐가 가장 큰 관건이라는 결론이 나왔다.

보통 다른 사람들이 구현한 직렬 포트 통신 프로그램을 보면 자체 버퍼가 존재했지만 내가 애써 프로그램 내에서 따로 버퍼를 구현하지 않으려고 했던 이유는 단지 프로그램 자체를 작고 간결하게 유지하고 싶어서였다. 그리고 직렬 포트의 버퍼를 내 마음대로 읽어올 수 있다면 크게 문제가 되지 않을것으로 보였다. 하지만 결국 내 프로그램에 자체 버퍼를 구현하게 된 가장 큰 이유는 직렬포트 버퍼를 내 마음대로 자유자재로 읽어오지 못했기 때문이다...

단순히 MSDN에서 제공하는 문서만 가지고서는 내가 원하는 만큼의 자유도로 버퍼를 읽어올 수 없었다. 그래서 Queue를 사용하여 버퍼를 구현했다. 구체적으로는 Array를 사용한 Circular Queue이고, 학교 다닐때 몇시간씩 시간을 투자해서 만들었던 queue를 단지 20분만에 후딱 만들어버렸다. 이젠 너무나 당연한 것일지도 몰라도 내 스스로도 놀랐다 ㅡ.ㅡ;



3. Solution
3.1 Basic Structure
내 프로그램의 구조는 이렇다. 일단 Windows API로 직렬 포트에 접근하는것을 간결하게 하기 위해 SerialPort이라는 wrapper class를 만들어서 직렬 포트로의 read, write을 담당할 메소드들을 생성했고, receive buffer의 역할을 담당할 Queue를 멤버를 가지고 있다.

그리고 Global Scope에서 SerialPort object를 매개변수로 받는 thread를 만들었다. 이 thread에서는 직렬포트가 열려있는 동안 무한 루프를 돌면서 직렬포트의 버퍼에 데이터가 수신되었는지 감시하여 데이터가 도착하면 Queue에 데이터를 enqueue하도록 하였다. 무작정 무한 루프를 돌리면서 직렬 포트 버퍼에서의 데이터 유무를 판단하는 Polling 방식을 사용하면 CPU cycle을 너무 많이 소비하기 때문에 WaitCommEvent()를 사용하여 평상시에는 잠들어있다가 직렬 포트에 데이터가 수신되었다는 event가 발생할 때에만 수신된 데이터를 Queue로 옮겨 넣는 작업을 수행하도록 했다.

3.2 Synchronization between threads and the process
Thread를 사용하게 됨에 따라 race condition이 발생하게 되는데, 크게 두 곳에서 발생할 수 있다. 한 곳은 직렬포트 자체의 버퍼이다. 또 한곳은 내가 구현한 queue에서 이다.

- 직렬 포트 버퍼에서 read, write operation에 대한 race condition 해결책
그동안 overlappedIO에 대한 개념이 잘 이해가 안갔지만 내가 이해한 것을 바탕으로 설명하면 직렬 포트 통신에서 overlappedIO를 사용하는 이유는 다음과 같다. 직렬포트에 자칫 rx buffer,와 tx buffer가 따로 존재한다고 생각하면 문제가 되지 않을 수 있다고 생각할 수 있을지도 모른다. 하지만, Windows API에서 직렬포트를 접근하는 방식은 file에 접근하는 방법과 똑같다. 파일에 동시에 읽고 쓸 수 없듯이 직렬 포트에서도 마찬가지이다. 그래서 overlapped structure를 사용해 read와 write시에 event를 발생하여 직렬 포트로의 read와 write operation이 동시에 직렬 포트에 접근하지 않도록 한다.

- SerialPort class 내에서 queue의 enqueue, dequeue operation에 대한 race condition 해결책
만약 수신된 데이터를 읽고 싶다면 SerialPort에 만들어둔 read 관련 메소드들을 사용하여 queue에 데이터를 dequeue 한다. 반면 직렬 포트를 감시하고 있는 thread는 데이터 수신시 queue에 데이터를 enqueue 한다. Main process와 thread에서 모두 SerialPort의 queue에 접근하기 때문에 스레드와 main process를 서로 동기화 시켜주어서 context switching이 발생하는 동안 queue에 서로 접근하는 것을 방지할 필요가 있었다. 그래서 Windows API에서 제공하는 CriticalSection object를 사용하여 queue class 내에서 queue의 data structure인 array에 접근할때는 항상 EnterCriticalSection(), LeaveCriticalSection()을 해주었다. 이렇게 하여 queue에 대해서 어느 순간에든지 한 process, 또는 thread만이 queue에 접근하도록 했다.


3.3 Exceptional Read Cases
경우에 따라서는 write operation으로 request를 보낸 후 read operation으로 응답을 바로 읽어오게 되는데 재빨리 응답이 오지 않는 경우가 있다. 그리고 응답이 오기까지의 시간은 정해져있지 않은 경우도 있다. 그런 경우에는 SerialPort object의 queue에 데이터가 채워질때까지 무작정 기다려야 한다. 그래서 Event Object를 만들어서 직렬 포트 감시 thread에서 데이터가 도착하면 event를 발생시켜 read operation에서 queue에 데이터가 없어서 기다리다가도 event가 발생하면 read operation을 재개하도록 했다.



4. Unsolved Mystery and Personal Opinion
뭔가 있는데 내 프로그램도 아직 제대로 이해 못하고 있어서 그런지 정리가 안되서 말 못하겠음... 하지만 간단히 얘기하자면 아무리 thread를 써서 모든 해결책을 제시한것 같아도 직렬 포트에서 read operation시 WaitTimeout을 INFINITE으로 설정해주면 WaitCommEvent가 발생하지 않는것 같다는것...
이번 작업을 계기로 MFC말고 Windows API에 눈을 뜨기 시작했다. 여태까지 들어온 말에 의하면 Windows API는 너무 어려워서 MFC를 사용하는거라고 하지만, GUI 작업이 아니면 오히려 Windows API를 사용하는것이 더 간결한 해결책을 제시하는것 같다고 생각한다. CCriticalSection, CEvent, AfxBeginThread() 이런거 싫다... 사실은 어디까지가 MFC이고 어디까지가 Windows API인지도 잘 모르겠다...

Windows app 만들때는 .Net 쵝오 b-.-


*소스코드도 올리고 싶은 마음이 간절하나 회사에서 작업한지라... 빼돌리기 쉽지 않아 소스코드 공개는 포기했음... 별것도 아니지만 외부로 빼내려면 참 골치 아프고 눈치 보여 소스 공개는 못하는 점이 아쉽다~


Posted by Dansoonie