Implementing IPC server for Testing
[원문보기]
이 글에서는 IPC server/client 구조로 프로그램 testing에 큰 도움을 받았던 글쓴이의 경험을 주절거려볼 것입니다. 먼저 말해 둘 것은, 이 글에서 다룰 내용은 실제 제품에 적용했었던 내용이기 때문에, 회사 규정상 다 밝힐 수 없습니다. 따라서 몇몇 부분은 실제와 다른 내용으로 쓸 것이고, 이 글에서 나온 코드는 저자가 따로 이 글에 맞게 새로 작성한 것입니다. (실제 제품 개발에 쓴 코드가 아님) 요약하면.. 알아서 걸러서 읽기 바랍니다. ^_^;;
IPC 서버의 필요성
먼저, 글쓴이의 상황에 대해 설명하겠습니다. 저는 예전에 Embedded Linux system에서 동작하는 GUI application을 작성한 적이 있습니다. 이 GUI application은 일종의 media player로서 여러가지 event를 받아서 image/video/audio를 연주하는 프로그램입니다. 이 application이 받는 event는 keypad input, memory card input(mount), UPnP등이 있습니다.
주어진 requirement에 따라 프로그램을 개발하던 도중, 여러가지 문제가 발생했었습니다. 기억나는 것만 추려보면:
- 새로운 기능 추가 요청이 들어왔을때, 이 기능을 구현하고, 간단히 테스트해보려 했으나, GUI에 해당 메뉴가 없었기 때문에, 새로 메뉴를 디자인하기 전에는 테스트가 불가능했습니다.
- 특정 상태에서 버그가 발생한다고 테스트 팀에서 연락이 왔을때, 매번 그 상태로 가기 위해 수많은 menu 조작 및 event가 필요했습니다. 즉, 버그가 발생하는 상황까지 가기 위해서 상당한 시간이 소모되었습니다.
- GUI application 외부에서 event가 발생하고, 이 event를 처리하기 위해, 적당한 방법을 찾지 못했습니다.
- 시스템이 멈췄을 때, 이 것이 GUI application이 hang된 것인지, keypad가 hardware 적으로 고장난 것인지 쉽게 알아차리기가 어려웠습니다.
글쓴이는 칼퇴근하는 것으로 악명높은? 개발자입니다. ^o^ 따라서, 위와 같은 문제로 시간을 보내는 것을 매우 싫어하는 사람입니다. 따라서 위와 같은 문제들을 쉽게 해결하기 위한 방법을 찾던 도중, GUI application을 IPC server로 만들면 어떨까하는 생각이 들었습니다. 그리고 상황에 따라 client 프로그램을 실행시켜 GUI application에 적절한 event를 날리고, GUI application이 이 요청을 처리할 수 있도록 하면, 대부분 문제가 쉽게 해결될 것으로 보았습니다.
어떤 IPC 방식을 쓸까?
IPC(Inter-process Communication)는 프로세스 사이에 통신을 의미하는 것으로 Unix system programming에 대해 공부해보았던 분이면 잘 알고 계실 것입니다. IPC는 여러가지 방법이 존재합니다. 대표적인 것만 나열해보면 다음과 같습니다:
- pipe
- FIFO (named pipe)
- shared memory (System V)
- message queue (System V)
- local domain socket
물론 이외에도 여러가지가 존재합니다만, 특정 Unix 시스템에 종속된 것이거나, Unix 종류에 따라 interface가 조금씩 달라지는 것이라서 제외되었습니다. 그래서 위와 같은 여러 IPC 방식 중 어떤 것을 써야하나하고 고민했습니다. 그래서 먼저 필요한 것들을 나열해보고, 이 조건을 만족하지 않는 것들을 제외시키기로 했습니다.
- 데이터를 주고 받는 것이 아니라 메시지를 주고 받을 것이기 때문에 bandwidth는 신경 안써도 된다.
- client는 항상 실행된 상태가 아니라 필요에 따라 실행된다. 즉 server/client 사이에 아무런 관계가 없다 -- pipe 제외
- server/client 사이에 서로 데이터를 주고 받을 수 있어야 합니다 -- pipe, FIFO 제외 (두 개를 만들면 되나, 귀찮다)
- 따로 동기화를 위해 코드를 작성하기 싫다. 즉 race-condition에 대해 따로 작업하기 싫다 -- shared memory 제외
- Client가 서버를 쉽게 판별할 수 있어야 한다. System V 방식은 모두 server가 파일에 key value를 기록하고 client가 읽어서 작업하는 것이 일반적이라서... -- System V 방식 IPC 모두 제외 (shared memory, message queue)
- server/client에 버그가 있더라도 server/client가 종료되었을 경우, system에 어떠한 resource leak가 있어서는 안된다 -- System V 방식 IPC 모두 제외
결국 선택된 것은 local domain socket이었습니다.
Process? Thread?
앞에서 설명한 것처럼, 제가 만든 application의 주 목적은 GUI를 제공하는 것입니다. 따라서 IPC server 기능을 추가한다고 해도, 이는 주 목적이 아닌 부가적인 기능이 되는 것입니다. 그렇다면 원래 기능에 영향을 주지 않고, IPC server 기능을 구현하려면 어떻게 해야 할까요?
대부분 GUI toolkit은 자체적으로 event loop를 가지고 있습니다. 즉 event가 있는지 없는지 항상 관찰하고 있다가, event가 발생하면, 적절한 정보와 함께 event queue에 이 event를 추가합니다. 그리고 application 개발자는 처리하고자하는 event에 대한 handler(또는 callback)를 추가하는 방식으로 프로그램을 작성합니다. 따라서 IPC 요청이 있는지 없는지 기다렸다가 요청이 들어오면, 해당 요청을 처리하는 코드를 추가하기가 애매해집니다. event loop가 가장 적당한 곳이기는 하지만, 이는 GUI toolkit에서 제공하는 것이기 때문에, application 개발자가 건드릴 수 없는 부분이기 때문입니다.
물론 GUI toolkit에 따라 idle event라는 것을 제공하기도 합니다. 즉 따로 event가 발생하지 않을 때 만들어지는 event죠. 하지만 이 event 안에서 무작정 IPC 요청을 기다릴 수는 없습니다. IPC 요청을 기다리는 부분을 non-blocking call로 만들면 가능하기는 하지만, 코드 작성이 매우 까다로워집니다. (IPC server 기능은 어디까지나 부가 기능이기 때문에, 복잡한 코드를 안 쓰는 것이 글쓴이의 목표입니다.)
따라서 IPC server 기능을 위해 별도의 process를 쓰거나 별도의 thread를 쓰는 것을 생각해볼 수 있습니다. 따로 process를 두는 것은 thread를 쓰는 것보다 더 안전하고, 개발/디버깅이 쉽다는 장점이 있습니다. 만약 IPC server 코드를 잘못 만든다고 해도 최악의 경우 IPC server process만 죽고 (예: SIGSEGV) 원 GUI application process는 정상 동작할 수 있기 때문입니다. 다만 문제가 되는 것이, IPC 요청이 대부분 GUI process의 상태에 대한 질문이거나, GUI process의 상태 변경 요청일 것인데, IPC server를 따로 process로 만들면, GUI process와 IPC server process 사이의 데이터 교환을 위해 따로 코드를 만들어야 합니다. 이는 매우 번거로운 작업이 될 것입니다.
Thread를 쓰는 것은 개발/디버깅이 복잡하다는 단점이 있습니다. 또한 IPC server thread를 잘못 만들 경우, 전체 process가 죽어버릴 수 있다는 risk도 있습니다. 하지만 어차피 대부분 GUI toolkit이 event handler/event loop를 별도의 thread로 관리하기 때문에, 이미 multi-threaded application에 새 thread를 추가하는 것은 큰 overhead가 되지 않습니다. 그리고 주의깊게 코드를 작성했다면, IPC server thread와 GUI thread 사이에 데이터를 교환하는 것이 매우 쉽습니다. (어차피 대부분 data가 thread들이 공유하는 것이기 때문에.)
따라서 IPC server 기능을 thread를 써서 구현하기로 결정했습니다.
IPC message format
IPC client가 GUI application에 전달하는 요청은 어떤 것들이 있을까하고 생각해보아야 합니다. 또한 이 요청 포맷에 대해서도 미리 결정해 두어야 합니다.
먼저 요청할 수 있는 기능에 대해 간단히 설계해봅시다:
- 현재 모드 (image/video/audio play 등) 상태 요청
- 현재 network information 요청 (IP address, router address, etc)
- 현재 mount된 device의 목록 요청
- key-event 발생 -- keypad 입력을 simulation
- network information 변경
- 기타 등등 (밝힐 수 없음 ^_^;;)
이제 메시지 포맷에 대해 알아봅시다. 가장 효과적으로 만들려면 binary 형태의 format을 만드는 것이 좋을 것입니다. endian 문제를 신경써야 하나하고 생각할지도 모르지만, 어차피 한 host에서 동작하는 것이기 때문에 신경쓸 필요가 없습니다. 하지만, 이리저리 디버깅하기도 귀찮고, 속도 신경쓸 일도 없기 때문에, text 형태로 메시지를 주고 받을 예정입니다.
기본적으로 모든 요청은 \n으로 구분되는 텍스트 한 줄로 처리할 것입니다. 또한 메시지 첫 단어가 '#'이면 주석 처리합니다. 즉 서버는 '#'로 시작하는 요청은 그냥 무시합니다. 위에 나열한 기능들을 처리하는 메시지 요청 format은 다음과 같습니다:
# 그냥 IPC 서비스 상태 확인 NOP # 현재 모드 상태 요청 INFO mode # 현재 network 상태 요청 INFO net # 현재 mount 상태 요청 INFO mount # key event 발생 (left arrow key down event) KEY down left # key event 발생 ('e' key up event) KEY up e # 현재 모드 XXX로 변경 SET mode XXXX # 현재 network 정보 변경 # ifconfig/route 뒷 내용은 각각 ifconfig/route 명령 형식 그대로 SET ifconfig ... SET route ... # IPC 요청 종료 BYE
IPC server는 위 요청을 받아 처리한 후, 다음과 같은 메시지를 출력합니다:
# 주어진 요청이 성공적으로 처리되었을 때: OK # 주어진 요청이 실패했을 때, 에러 문자열과 함께 출력: ERR: permission denied
Server
사실 이 글을 쓰면서 server쪽에 무슨 말을 해야할까 고민했습니다. 따로 관련 코드만 뽑아내기도 상당히 애매할 뿐만 아니라, 코드 양이 이 글에서 전부 다룰만큼 작은게 아니라서 다 다루기가 번거롭기 때문입니다. 따라서 과감히? 실제 코드는 생략하겠습니다. 이 글을 읽고 이해할 수 있는 분이라면 이미 TCP/IP socket은 충분히 다루어 보셨을 테고, 또 pthread에 관한 지식도 충분하실 것입니다. 따라서 관련 책/문서를 살펴보시면 쉽게 local domain socket을 써서 IPC server/client를 만드실 수 있을 것입니다.
이 section에서는 IPC server thread를 만들때 고려해야 할 사항을 소개하는 것으로 마칠까 합니다.
첫째, 대부분 GUI toolkit이 비슷한 상황인데, GUI toolkit은 multi-thread에서 동작하기 힘듭니다. 대부분 GUI toolkit이 thread-safe라고 선전하지만, 실제로는 가능한 한 thread에서 GUI toolkit을 호출해야하며, 부득이한 경우, 두개 이상의 thread에서 GUI toolkit을 호출해야 하는 경우, mutex나 semaphore 등을 써서 동기화해주어야 합니다. GUI toolkit에 따라 이러한 목적으로 쓸 수 있는 mutex나 semaphore 또는 관련 함수를 제공하는 것도 있으니, 관련 문서를 잘 살펴보아야 합니다. 어쨋든, 가능한 IPC server thread에서는 GUI toolkit을 호출하지 않도록 해야 합니다.
둘째, IPC server thread는 CPU clock을 많이 잡아먹으면 안됩니다. CPU clock을 많이 소모한다는 것은 원래 이 application이 해야 할 GUI thread의 resource가 줄어드는 셈이 되므로, 용납할 수 없는 것이 됩니다. 명백히 processor를 양보하는 콜인 sched_yield()를 쓰거나, select(), accept()등의 blocking call을 적절히 활용해서, IPC client가 메시지를 보내 처리하는 상황을 제외하고는 CPU clock을 쓰지 않도록 해야 합니다. 또한 IPC로 요청할 내용은 가능한 시간이 오래 걸리지 않는 내용으로 설계해야 합니다.
세째, 글쓴이가 만들고자 하는 IPC server/client는 application을 control하고 testing을 쉽게하기 위한 것이므로, multiple client에 대한 고려가 필요없습니다. 물론 설계를 잘한다면 충분히 multiple client들을 처리할 수 있겠지만, 글쓴이가 처한 상황에서 이는 시간적 낭비입니다. 따라서 multiple request를 처리할 수 있는 전형적인 TCP server와는 달리 IPC server thread는 단순히 bind(2) 후에 file descriptor를 그냥 쓰거나, listen(2)의 두번째 인자로 1을 쓰는 것이 좋습니다. 궂이 multiple client를 처리할 수 있는 IPC server thread를 만들겠다면 , 그리고 각 client마다 새로 thread를 만들어서 작업하겠다면, 위와 같은 이유에서 대부분 thread가 sleep 또는 block된 상태를 유지할 수 있도록 개발해야 합니다.
네째, 개발 환경이 debugger를 지원하는지 여부도 IPC server/client의 설계에 큰 영향을 미칩니다. 만약 개발 환경이 충분히 빠른 debugging 환경을 제공한다면, debugger로 처리할 수 있는 내용을 IPC 요청 명령으로 만들 필요가 없습니다. 이는 시간 낭비입니다. 하지만, debugger를 쓸 수 없는 상황이거나, debugger를 쓸 수 있어도 충분히 느린 상황이라면 debugger 역할을 할 수 있는 기능을 IPC 요청으로 구현하는 것도 좋습니다. 예를 들어, GUI application이 내부적으로 유지하는 상태 정보 등을 알아보기 쉽게 출력하는 기능을 만드는 것입니다.
Client
Client application은 다른 기능이 없고, 단순히 IPC server에 필요한 서비스를 요청하고 서버의 응답을 출력하는 프로그램입니다. 실행 방법은 크게 두 가지인데, 하나는 command-line에서 직접 한 명령을 내리는 것이고, 다른 하나는 실행 후, 필요한 명령을 계속해서 내릴 수 있는 방법입니다. 전자의 경우, 주어진 명령을 실행하고 나서 바로 종료하며, 후자의 경우 BYE 명령을 내릴 때까지 메시지를 받아서 처리합니다. 예를 들면 다음과 같습니다:
$ gclient info mode ... OK $ _
$ gclient gclient version 0.1 (c) Sxxxxxg Electronics 2005 (gclient) info mode ... OK (gclient) info network DEV: /dev/eth0 IP: 172.0.0.4 GATEWAY: 172.0.0.1 ... OK (gclient) set ifconfig ... ... OK (gclient) bye $ _
이때, 다양한 기능을 추가하면 gclient 프로그램의 활용도를 높일 수 있습니다.
- 미리 IPC 명령을 script 파일로 만들어 두고, "-s file" 형태의 옵션으로 읽어서 처리하는 기능
- 마지막 IPC 명령의 수행 결과에 따라 gclient의 exit code를 적절하게 활용
- "-q" 옵션으로 불필요한 정보 출력을 최소화 (꼭 필요한 것만 출력)
위와 같이 만들어 두면 나중에 shell script로 여러가지 작업을 처리할 수 있습니다.
Networking Setting Script
예를 들어 device에 IP address가 주어지지 않았을 경우, 자동으로 DHCP client를 실행시키는 script를 만들 수도 있습니다 (물론 완성 단계에서는 이러한 코드는 main application에 포함될 것이지만, 그 전까지는 이러한 utility를 써서 원하는 기능을 해낼 수 있습니다. 그리고 이러한 코드가 여러분의 시간을 매우 아껴줄 것입니다):
#!/bin/sh # Run DHCP client daemon if the IP address is not set. gclient -q info network | grep ^IP >&/dev/null if test "$?" -ne 0; then /sbin/dhcpcd eth0; fi
Load Testing Script
Testing 팀에서 메뉴 조작시 마구잡이로 빠른 속도로 Left/Right/Down/Up key를 누르면 application이 죽는다고 연락이 왔습니다. 먼저 이런 상황에서는 key pad driver가 문제인지, GUI application이 문제인지 쉽게 확인할 수가 없습니다. 이 경우에도 다음과 비슷한 방법으로 문제를 일으키는 script를 만들 수 있습니다:
#!/bin/bash # # ltest-dirkey.sh: Load test from random direction key event # function random_dir() { num=`dd if=/dev/random bs=1 count=1 2>/dev/null | od -A n -t uC` dir=`expr $num % 4` keynames=("LEFT" "RIGHT" "UP" "DOWN") echo ${keynames[$dir]} } if test $# -ne 1; then echo "usage: $0 REPEAT-COUNT" exit 1 fi count=0 while test $count -lt $1; do kname=`random_dir` echo "DIR($count): $kname" gclient -q KEY down $kname count=`expr $count + 1` done
코드를 잠깐 설명하면, 먼저 bash 함수 random_dir은 랜덤으로 "UP", "DOWN", "LEFT", "RIGHT" 중의 한 단어를 출력하는 함수입니다. dd(1)와 /dev/random, od(1)를 써서 random number를 만들어 내는 것을 알아두기 바랍니다. 간단히 말해, 이 스크립트는 첫번째 인자로 전달한 횟수만큼 gclient를 이용해서 방향키 event를 서버에 전달합니다. 예를 들어 방향키 event를 5번 랜덤으로 발생시키려면 다음과 같이 실행하면 됩니다:
$ test/ltest-dirkey.sh 5 DIR(1): LEFT DIR(2): UP DIR(3): UP DIR(4): LEFT DIR(5): LEFT $ _
위 test 프로그램을 여러번 돌려서 문제가 발생한다면 GUI application에 문제가 있다는 것을 알 수 있습니다. 만약 위 프로그램을 여러번 돌려서 문제가 발생하지 않았다면 이 system의 key pad driver쪽을 의심해봐야겠죠. 밤을 새며 system의 key pad를 눌러보며 debugging하는 대신, 위 프로그램 몇번 돌려보고 문제가 발생하지 않았다면 (그리고 key pad driver 개발자가 아니라면) 퇴근하면 됩니다. ^o^
New Feature Testing
새로운 기능이 GUI application에 추가된다거나, 혹은 외부 event를 처리해야 할 경우가 종종 발생합니다. 예를 들어 embedded system에 USB storage가 연결되었다거나 ethernet card를 통해서 특별한 요청이 들어올 경우 등을 말합니다. 물론 이러한 기능 처리는 GUI application이 직접 처리할 수 있어야 합니다. 하지만 개발 도중에 이러한 코드를 GUI application에 직접 작업하는 것은 쉬운 일이 아닙니다. 자칫 지금까지 만들어 논 기능이 제대로 동작하지 않는다거나, 알 수 없는 이유로 application이 hang될 수 있기 때문입니다.
앞에서도 언급했지만, 이러한 다양한 event 처리를 위해 GUI toolkit의 event loop 코드를 직접 고치는 것은 어렵기도 하지만, 버그가 발생할 소지가 많습니다. 다행히 글쓴이가 작업하는 system은 keyboard가 없는 system입니다. 단지 몇 개의 키만 제공하는 key pad가 있을 뿐입니다. 달리 말해 keyboard input event의 대부분이 놀고 있다는 뜻입니다. 즉 알파벳/숫자를 입력할 키보드가 없으니 [a-z0-9] key down과 같은 event는 발생할 일이 없는 것입니다. 따라서 글쓴이가 작업하는 시스템에서는 이러한 key event를 다른 event 용으로 만들어 쓸 수 있었습니다.
keyboard가 있는 시스템에서는 주어진 시스템에서 발생할 확률이 없는 event로 대체하거나 GUI toolkit에 따라 user-defined event를 만들 수 있는 방법을 쓰면 됩니다.
예를 들어, automounter에서 USB storage가 연결된 경우, GUI application에 이를 notify할 방법이 필요합니다. 물론 마지막 단계에서는 좀 더 효과적인 방법을 써야 하겠지만, 초기 개발 단계에서는 gclient를 활용할 수도 있습니다. 즉, GUI thread의 event handler에서 'm' key를 처리하는 event handler를 만들어서 mount 처리 하는 코드를 집어넣습니다. 그리고 automounter에서 system(3)과 같은 함수를 써서 아래와 같이 호출합니다.
system("gclient KEY down m"); /* Send mount event to the GUI application */
다시 말하지만, 이 방식은 효율적이지 못하므로, 초기 feasibility testing에서나 쓸 수 있는 방식입니다. 결국 언젠가는 바꿔야하겠지만, 당분간 좀 더 생산적인 일에 시간을 투자할 수 있도록 도와줄 것입니다.
References
이 글에서 나온 내용에 대하여 좀 더 자세히 알아보고 싶다면 아래 책들을 참고하기 바랍니다.
- W. Richard Stevens, UNIX Network Programming, Volumn 1: Networking APIs: Sockets and XTI (2nd edition), Addison Wesley, 1998, ISBN 0-13-649328-9
- W. Richard Stevens, Advanced Programming in the UNIX Environment, Addison Wesley, 1992, ISBN 0-201-56317-7
- David R. Butenhof, Programming with POSIX Threads, Addison Wesley, 1997, ISBN 0-201-63392-2
- Marc J. Rochkind, Advanced UNIX Programming (2nd edition), Addison Wesley, 2004, ISBN 0-13-141154-3
- Brian W. Kernighan, Rob Pike, The Practice of Programming, Addison Wesley, 1999, ISBN 0-201-61586-X
댓글
여건에 따라 다르지만...
저는 표준입출력(stdin,out)을 선호하는 편입니다.
주로 testbed에 ssh등으로 접속해서 어플리케이션을 실행하면,
testbed가 실행되는 동안에도 원격환경의 콘솔을 통해서 모든 것을 조작하거나
면밀하게 검토할 수 있기 때문입니다. (구현도 비교적 간단하고...)
표준입출력은 별도의 처리없이 쉽게 pipe로 대체가 가능하다는 면이 있고,
이를 이용해 간단한 tcp 데몬을 이용하면 shell,ssh와 같이
덩치가 커다란 원격접속 환경을 제공하지 않는 경우에도
telnet으로 접속해서 사용할 수 있기 때문이죠.
mplayer의 slave 모드나 xine의 리모트기능과 유사하게 구현합니다.
There is no spoon. Neo from the Matrix 1999.
There is no spoon. Neo from the Matrix 1999.
댓글 달기