리눅스 네트워크 프로그래밍
2부:데몬 프로세스 생성하기
글/ Ivan Griffin과 John Nelson 역/ 이기동(자유기고가)
데몬 프로세스는 여러 클라이언트에 서비스하기 위해서 백그라운드로 실행되는 서버이다. 여러분은 데몬 프로세스를 생성할 때 다음과 같은 몇 가지 사항에 주의하여야 한다. 개발하는 동안 디버깅할 때 printf나 write를 사용할 수 있도록 서버를 포그라운드로 실행하는 것이 좋다. 또한, 서버가 오동작 한다면 중단 문자(보통 CRTL-C)를 보내어 프로세스를 끝낼 수 있다는 장점도 있다. 실제
로 사용할 수 있게 되었을 때 서버는 데몬처럼 동작하도록 작성하여야 한다. 유닉스에서 데몬 프로그램은 보통 문자 d로 끝난다. 예를 들어, HTTP 서버(웹 서버)는 httpd이다.
데몬을 실행시킬 때 자동적으로 백그라운드로 수행하도록 하는 것이 좋다. 이는 fork() 호출을 사용하면 간단하다. 제대로 동작하는 데몬은 fork를 수행한 후에 부모로부터 물려받은 파일 디스크립터를 모두 닫는다. 파일이 터미널 장치인 경우 특히 중요한데 데몬을 실행한 사용자가 로그아웃할 때 터미널 상태를 초기화하려면 장치를 닫아야만 한다. 열고 닫을 수 있는파일 개수의 최대
값을 결정하려면 getrlimit() 호출을 사용한다.
그 다음 프로세스는 프로세스 그룹을 변경한다. 프로세스 그룹은 시그널을 보내는데 사용된다. 터미널에서 같은 그룹에 속한 프로세스는 포그라운드로 수행되고 터미널에서 보내는 데이터를 읽을 수 있도록 한다. 다른 그룹에 속한 프로세스는 백그라운드로 수행된다고 간주한다(그리고 이렇게 읽으려고 하면 프로세스의 수행이 잠시 중단될 것이다).
제어 터미널을 닫고 세션 그룹을 변경하는 것은 데몬 프로세스가 이전 그룹 리더(보통 셸이 된다)로부터의 잠재적인(예를 들어, 사용자가 kill 명령으로 보내지 않은) 시그널을 받는 것을 막는다. 프로세스는 세션에 속한 프로세스 그룹으로 구성되어 있다. setsid() 시스템 호출로 그 프로세스를 세션 리더로 하는 새로운 세션(새로운 프로세스 그룹)이 만들어지게 된다.
데몬이 제어 터미널을 잃게 되면 다시 얻어서는 안된다. 프로세스 그룹 리더가 새로운 터미널 장치를 열 때 제어 터미널을 자동으로 얻는다. 이를 막는 가장 쉬운 방법은 setsid()를 호출한 후에 fork()를 다시 호출하는 것이다. 데몬은 두 번째 자식 프로세스로 실행되게 된다.
부모 프로세스(세션과 프로세스 그룹 리더)의 실행이 끝나면 두 번째 자식은 0번의 새로운 프로세스 그룹을 얻게 된다(init의 자식 프로세스가 되기 때문이다). 따라서, 새로운 제어 터미널을 얻을 수는 없는데 그것은 프로세스 리더가 아니기 때문이다. 다양한 표준 라이브러리 루틴은 세 가지 표준 입출력 디스크립터가 열려 있다고 가정하기도 한다. 결과적으로 서버는 보통 세 가지 모든 디스크립터를 열어 두고, /dev/null과 같이 시스템에 영향을 주지 않는 I/O 장치에 연결되어 있다.
데몬은 보통 부트할 때 시작하고 시스템이 가동하는 시간 동안 줄곧 실행중인 상태로 있다. 데몬이 마운트된 파일 시스템에서 시작하였다면 데몬을 죽이기 전까지 파일 시스템을 마운트를 해제하는 것이 불가능하다. 이를 고려하여 데몬 프로그래밍을 할 때 chdir()을 수행하여 /로 놓는 것이 현명하다(또는 데몬의 동작과 관계가 있는 파일이 있는 파일 시스템으로 놓아도 된다).
데몬은 부모 프로세스의 umask 정보를 물려받는다. 나중에 데몬에서 파일을 생성할 때 이를 방지하기 위해서는 보통 umask()를 사용하여 0으로 놓는다.
리스트 1의 예제 코드에서 이러한 점을 설명하고 있다.세션을 지원하지 않는 시스템(예를 들어 리눅스, 솔라리스와 다른 몇몇 시스템)에서는 리스트 2의 코드를 사용하여 setsid()와 같은 결과를 얻을 수 있다.
주 서버 코드에서 프로세스를 복제하여 만들어진 자식 프로세스가 종료하면 할당된 메모리를 회수하지만 프로세스 테이블의 엔트리에서는 제거되지 않는다.
바꿔 말하면, 프로세스가 죽게 되면 예를 들어 시스템 자원을 소모하지는 않지만 그들을 완전히 되돌려 받는 것은 아니다. 그들이 좀비와 같은 형태로 돌아다니는 이유는 부모 프로세스가 필요한 경우(예를 들어, CPU 사용량 등) 자식 프로세스로부터 통계를 수집할 수 있도록 하기 때문이다. 분명히, 데몬은 프로세스 테이블이 좀비 프로세스로 가득 차는 것을 원하지는 않을 것이다.
자식 프로세스가 죽게 되면 부모 프로세스에 SIGCHLD 시그널을 보낸다.
부모가 자식을 강제로 회수하지 않는 한, 시그널의 기본 핸들러가 자식을 좀비로 바꾼다. 이는 리스트 3에서 볼 수 있다. 대안으로 리스트 4에서 볼 수 있듯이 시그널을 무시하고 좀비가 죽도록 할 수 있다.
데몬이 대부분의 다른 시그널을 무시하거나 SIGHUP을 받은 후에 설정 파일을 다시 읽고 재시작 하는 일도 자주 있다. 많은 데몬은 자신의 PID(프로세스 번호)를 로그 파일에 저장한다, 보통은 /var/run/foobar.pid(foobar는 데몬의 이름이다)가 되는데 프로세스를 중단하는 경우 도움을 준다.
시스템을 셧다운 할 때(또는 다중 사용자에서 단일 사용자 모드로 바뀔 때), 모든 프로세스에게 알리기 위해서 SIGTERM 시그널을 보낸다. 그 다음 init 프로세스는 정해진 시간만큼 기다린다(SVR4에서는 20초, BSD에서는 5초, 리눅스 init에서는 5초, 리눅스 셧다운(shutdown)에서는 기본값으로 3초이지만 명령행 인자에서 지정 가능) 프로세스가 계속 살아 있으면 무시되지 않는 시그널인
SIGKILL 시그널을 보낸다.
따라서, 다른 프로세스가 정상적으로 종료하는지 확실히 하려면 데몬 프로세스가 SIGTERM 시그널을 가로챌 수 있어야 한다.
------------------------------------------------------------
리스트 1 : 데몬 시작 코드
/* Listing 1:
* fork(), closing controlling terminal, chang
* ing session group, fork(), change current
* working directory, set umask Ivan Griffin
* (ivan.griffin@ul.ie) */
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <unistd.h>
int main(int argc, char *argv)
{
struct rlimit resourceLimit = { 0 };
int status = -1;
int fileDesc = -1;
/* somewhere in the code */
status = fork();
switch (status)
{
case -1:
perror(fork());
exit(1);
case 0: /* child process */
break;
default: /* parent process */
exit(0);
}
/* child process */
resourceLimit.rlim_max = 0;
status = getrlimit(RLIMIT_NOFILE, &resourceLimit);
if (-1 == status) /* shouldn't happen */
{
perror(getrlimit());
exit(1);
}
if (0 == resourceLimit.rlim_max)
{
fprintf(Max number of open file
descriptors is 0!!\n);
exit(1);
}
for (i = 0; i < resourceLimit.rlim_max; i++)
{
(void) close(i);
}
status = setsid();
if (-1 == status)
{
perror(setsid());
exit(1);
}
status = fork();
switch (status)
{
case -1:
perror(fork());
exit(1);
case 0: /* (second) child process */
break;
default: /* parent process */
exit(0);
}
/* now we are in a new session and process
* group than process that started the
* daemon. We also have no
* controlling terminal */
chdir("/");
umask(0);
fileDesc = open("/dev/null" O_RDWR);
/* stdin */
(void) dup(fileDesc); /* stdout */
(void) dup(fileDesc); /* stderr */
/* the rest of the daemon code executes in this environment */
return 0;
}
---------------------------------------------------------------
리스트 2 : sutsid()를 지원하지 않은 시스템에서 쓰이는 데몬 코드
/* Listing 2:
* change process group for systems without
* sessions Ivan Griffin (ivan.griffin@ul.ie) */
#ifdef BSD
{
int fd;
setpgrp(0, getpid());/* change process group */
/* open controlling terminal */
fd = open(/dev/tty, O_RDWR);
if (-1 != open)
{
/* lose controlling terminal */
ioctl(fd, TIOCNOTTY, 0);
close(fd);
}
}
#endif
#ifdef SVR4
/* change process group AND lose controlling
* terminal */
setpgrp();
#endif
-----------------------------------------------------------
리스트 3 : (죽은) 자식 프로세스를 강제로 회수하는 코드
/* Listing 3:
* Explicitly reaping the child:
* Ivan Griffin (ivan.griffin@ul.ie) */
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
void ReapChild(int pid);
struct sigaction reapAction =
{ ReapChild, /* SIG_DFL for default, SIG_IGN to ignore, else handler */
0, /* mask of signals to block during execution of handler */
SA_RESTART, /* don't reset to default handler after signal is raised */
NULL /* Not used - should be NULL */
};
int main(int argc, char *argv[])
{
/* somewhere in main code */
sigaction(SIGCHLD, &reapAction, NULL);
/* rest of code */
return 0;
}
void ReapChild(int pid)
{
int status;
wait(&status);
}
--------------------------------------------------------------
리스트 4 : SIGCHLD 시그널 무시하기
/*Listing 4:
* Ignoring the SIGCHLD signal
* Ivan Griffin (ivan.griffin@ul.ie) */
#include <signal.h>
struig action ignoreChildDeath =
{
NULL, 0, SA_NOCLDSTOP | SA_RESTART, NULL
};
int main(int argc, char *argv[])
{
/* somewhere in main code */
sigaction(SIGCHLD, &ignoreChildDeath, NULL);
/* other code */
return 0;
}
----------------------------------------------------------
네트워크 데몬 설계
그림 1에서는 클라이언트에 네트워크 서비스를 제공하는 데몬에서 가능한 세 가지 설계 방식을 보여준다. 첫 번째 그림에서 데몬은 프로세스를 복제하여 각 프로세스의 요청을 처리하는 가장 일반적인 방법을 따른다. 처리하는 동안 부모는 새로운 접속 요청을 받는다. 이러한 동시 처리 기법은 계속되는 요청에 대응할 수 있다는 장점이 있고 차례대로 처리하는 것보다 더 나은 성능을 낼
수 있으며 반복적으로 서비스를 처리한다. 불행히도 커널은 커널모드와 사용자 모드를 전환해야 하고 프로세스 복제가 필요하여 커다란 부하가 걸리므로 이러한 접근 방법은 대단히 많은 요청을 처리해야 하는 서버에는 부적합하다.
두 번째 도표는 또 다른 요구를 처리하기 전에 서버가 하나의 프로세스만을 실행하면서 반복적, 동기적으로 요청을 받아들이고 처리하는 것을 보여준다. 이러한 접근 방법은 요청이 처리되는 동안 발생하는 또다른 요청이 잠시 중단되거나 거부된다는 약점이 있다. 잠시 중단된다면 중단되는 시간의 대부분은 요청을 처리하고 통신하는 데 걸리는 시간이다.
이러한 지연 시간에 따라, 듣기 큐(queue)가 가득 차 있을 때 상당한 양의 요청이 거부될 수 있다. 따라서, 이러한 접근 방법은 지연 시간이 매우 짧은 요청을 처리하는데 매우 적합하다. 또한 TCP 데몬보다는 UDP 네트워크 데몬에 잘 맞는다.
프로세스 미리 할당하기
그림 1의 세 번째 도표는 가장 복잡하다. 이는 데몬이 요청을 처리하기 위해서 새로운 프로세스를 미리 할당하는 것을 보여준다. 주 프로세스가 listen()다음에 fork()를 호출하지만 accept() 이전에호출한다는 것에 주의한다. 부모 프로세스가 accept()를 호출한다. 이러한 시나리오는 accept() 호출을 잠시 중단시키는 동시에 이용할 수 있도록 대기하는 서버 프로세스를 남겨 둔다. 그러나, 커널은 주어진 요청에서 오직 하나의 부모 프로세스만이 accept() 호출에 성공할 것이라는 것을 보증한다. 요청에 서비스한 다음에 accept 상태를 리턴한다. 주 프로세스는 종료하거나(SIGCHLD는 무시한다) 부 프로세스가 종료하는 것을 회수하기 위해서 계속 wait()를 호출할 수 있다.
부모 프로세스가 자신을 죽이기 전에 메모리 낭비를 막기 위하여 보통 정해진 개수의 요청을 받아들인다. 받아들이는 요청의 수가 가장 낮은 프로세스가 새로운 프로세스를 필요한 만큼 생성할 것이다. 널리 쓰이는 웹서버에서 서버 스레드를 미리 할당하는 이러한 기법을 구현하고 있다(예를 들어 넷스케이프 서버, 아파치 서버가 있다).
지연되는 프로세스 할당
일반적인 경우 요청을 처리하는 서버 프로세스의 수행 시간이 매우 짧을 때, 항상 동시에 처리할 필요가 있는 것은 아니다. 반복적인 서버는 커널이 커널 모드와 사용자 모드를 전환하는 부하를 줄임으로써 더 나은 성능을 보인다. 동시적 처리와 반복적 처리의 설계를 결합한 방법은 새로운 서버의 할당을 지연하는 것이다. 서버는 요청을 반복적으로 처리하기 시작한다. 요청에 대한 처리
시간이 상당히 걸린다면 그 처리를 마무리하기 위해서 각각의 부 프로세스를 생성한다. 따라서, 주 프로세스는 새로운 부 프로세스를 생성하기 전에 요청이 유효한지 점검하고 또는 처리 시간이 짧은 요청을 처리한다.
지연된 프로세스 할당을 사용하기 위해서는 리스트 5에서 볼 수 있는 것처럼 alarm() 호출을 사용한다. 주 프로세스에서 타이머를 생성하고, 시간이 초과되면 시그널 핸들러가 호출된다. fork() 시스템 호출은 핸들러 안에서 수행된다.
부모는 요청 받는 접속 통로를 닫고 accept 상태를 리턴한다. 반면에 자식 프로세스는 요청을 처리한다. setjmp() 시스템 호출은 프로세스 스택 환경의 상태를 기록한다. longjmp()가 나중에 호출될 때, 프로세스는 setjmp()로 저장된 것과 같은 상태로 복구한다. longjmp()의 두 번째 인자는 스택이 복구될 때 setjmp()에서 리턴된 값이다.
-----------------------------------------------------------
리스트 5 : 지연된프로세스 할당
/* Listing 5:
* delayed the allocation of new server
* processes Ivan Griffin (ivan.griffin@ul.ie) */
#include <unistd.h>
#include <setjmp.h>
#include <signal.h>
static void _AlarmHandler(int);
jmp_buf env = { 0 };
const int NUMBER_SECONDS = 5;
/* depends on particular application */
struct sigaction alarmAction =
{
_AlarmHandler, 0, SA_RESTART, NULL
};
int main(void)
{
/* usual daemon/socket stuff goes here*/
sigaction(SIGALRM, &alarmAction, NULL);
for (;;)
{
/* block here on accept() call*/
(void) alarm(NUMBER_SECONDS);
if (setjmp(env) != SIGALRM)
/* if SIGALRM is returned => parent */
{
/* request processing is performed here if slave, perhaps exit at end? */
}
}
/* never reached */
return 0;
}
void _AlarmHandler(int signal)
{
int pid = fork();
switch (pid)
{
case -1: perror("fork()");
exit(1);
break;
case 0: /* child */
break;
default: /* parent */
longjmp(env, SIGALRM); /* indicate by returning SIGALRM */
break;
}
}
-------------------------------------------------------------
스레드
예제의 프로세스 복제는 새로운 실행 스레드를 생성할 때 프로세스 전체를 모두 복제하는 대신 pthread_create()를 호출할 수 있다. 이전에 설명했듯이 한 스레드에서의 I/O 블럭이 다른 스레드의 CPU 사용권을 모두 점유하지 않도록 하려면 스레드는 커널 수준 스레드가 되어야 한다.
여기에는 Xavier Leroy 씨의 훌륭한 커널 수준 리눅스 스레드 패키지(http://pauillac.inria.fr/~xleroy/linuxthreads/)가 있는데 이는 clone() 시스템 호출에 기반을 두고 있다.
스레드를 이용하여 구현하는 것은 다중 프로세스 모델을 사용하는 것보다 더 복잡한 문제가 나타난다. 당연한 이야기지만 스레드를 사용하는 것은 커널이 커널 모드-사용자 모드를 전환하는 시간과 메모리 사용량을 효과적으로 줄일 수 있다. 파일 디스크립터를 이용하거나임계영역의 보호와 같은 다른 문제도 나타난다.
대부분의 운영체계에서는 프로세스가 열 수 있는 파일 디스크립터의 개수를 제한하고 있다. 프로세스가 getrlimit()와 setrlimit() 호출을 사용하여 이를 시스템 차원에서 최대값으로 늘릴 수 있지만 이 값은 보통 /usr /include/ sys/param.h 파일의 NOFILE에 256으로 정의되어 있다.
NOFILE과 /usr/src/linux/include/lin ux/fs.h 파일의 NR_OPEN과 NR_FILE을 고쳐서 커널을 재컴파일 하는 것은 별 도움이 되지 않을 수도 있다. 리눅스 FILE 구조체의 fileno 요소(리눅스에서는 보통 _fileno로 불린다)의 자료형은 INT로 이는 보통 다른 시스템에서 unsigned char가 되므로 버퍼링 하는 입출력 함수(fopen(), fprintf() 등)의 파일 디스크립터의 개수를 255개로 제한한다.
이러한 차이는 응용 프로그램의 이식성에 영향을 미친다.
스레드는 메모리 공간을 공유하여 사용하므로, 그 공간이 일관성 있는 상태로 유지되고 각자의 메모리 영역을 침범하지 않는지 확실히 하는 주의가 필요하다. 이는 여러 개의 스레드(임계 영역)가 공유하면서 접근할 수 있는 자료를 기록하는데 차례를 맞출 필요가 있다는 것을 의미한다. 이는 락(lock)을 이용하여 구현할 수 있지만 데드락 상태에 들어가지 않도록 주의를 기울일 필요가 있다.
init 에서 일어나는 문제
init의 주된 역할은 /etc/inittab에 저장된 정보를 읽어 프로세스를 생성하는 것이다. 이는 직접적으로 또는 간접적으로 시스템에서 사용자가 생성한 모든 프로세스에 책임을 진다. 프로세스가 죽었을 때는 다시 시작하도록 할 수도 있다.
init가 프로세스를 생성하는 능력은 리스트 1에서 데몬이 프로세스를 복제하는 경우 상당히 이해하기 어렵게 된다. 자식 데몬은 계속 실행되면서 최초의 데몬 프로세스는 바로 종료하고, init는 데몬이 죽은 것을 의미로 이를 가져온다. 간단한 해결책은 데몬에게 프로세스 복제하는 코드를 피한다는 것을 알려주기 위하여 데몬에 명령행 스위치(아마 -init가 될 것이다)를 덧붙이는 것이다. 더 나
은 해결책은 데몬이 /etc/inittab에서보다 /etc/rc 스크립트에서 시작하도록 하는
것이다.
SVR4 형태의 /etc/rc
시스템 V 형태의 /etc/rc 배치는 널리 쓰이는 리눅스 배포본인 레드햇과 데비안에서 쓰인다. 이들 시스템에서 시작/종료 할 수 있는 각 데몬은 레드햇의 경우 /etc/rc/init.d, 데비안의 경우 /etc/init.d에 스크립트가 있다. 이 스크립트는 하나의 명령행 인자인 start와 함께 데몬을 시작하도록 하고 stop 인자를 주어 데몬의 실행을 멈출 수 있다. 이 스크립트는 일반적으로 데몬에 따라 이름을 붙인다.
여러분이 특정한 실행 단계에서 데몬을 시작하기를 원한다면 실행 단계 디렉터리에서 /etc/rc/init.d에 있는 적당한 스크립트에 링크를 걸어 줄 필요가 있다.
여러분은 시작 링크의 이름을 Sxxfoobar이라 붙여야 하는데 foobar는 데몬의 이름이고 xx의 두자리수의 숫자이다. 이 숫자는 스크립트가 실행하는 순서를 배열하는데 쓰인다.
이와 비슷하게 여러분이 특정한 실행 단계로 변경할 때 데몬의 실행이 끝나기를 원한다면 실행 단계 디렉터리에서 /etc/rc/init.d 스크립트에 해당하는 링크를 걸어주어야 한다. 이러한 링크는 Kxxfoobar로 이름 붙여야 하는데 시작 링크와 같은 규칙을 따른다.
시스템 관리자가 (/etc/rc/init.d에서 적당한 스크립트를 호출하고, 알맞은 명령행 인자를 가지고)데몬을 시작/종료하는 것은 이전의 BSD 방식의 /etc/rc.d 배치보다 훨씬 유연성을 지니고 있으며 시스템 V의 구조에서 더 향상된 장점 중 하나이다.
리스트 6의 셸 스크립트는 일반적인 레드햇 방식으로 /etc/rc/init.d의 foobar 데
몬을 다루는 예제를 보여준다.
----------------------------------------------------------
리스트 6 : SysV init 스크립트 예제
#!/bin/sh
# Listing 6:
# Sample SysV init script
# Ivan Griffin (ivan.griffin@ul.ie)
# Source Red Hat function library
. /etc/rc.d/init.d/functions
# Source networking configuration.
. /etc/sysconfig/network
# Check that networking is up
[ ${NETWORKING} = no ] && exit 0
case $1 in
start)
echo -n Starting daemon foobar:
daemon foobar
echo
;;
stop)
echo -n Shutting down daemon foobar:
killproc foobar
echo
;;
*)
echo Usage: foobar {start|stop}
exit 1
esac
-----------------------------------------------------------
syslog() 사용하기
데몬이 디버깅하거나 시스템 관리/유지 보수를 위해서 자신의 활동을 기록하는 기능은 매우 유용하게 쓰인다. 이는 파일을 열고 일어난 사건을 그 파일에 기록함으로써 이루어진다. 많은 리눅스 데몬은 데몬의 상태와 정보를 기록하기 위해서 syslog() 호출을 사용한다. syslog는 클라이언트 서버 기록 기능으로 원래는 BSD 4.2에 기반을 둔다. SVR4나 POSIX에서 같은 기능을 하는 것은 없
는 것으로 보인다. syslog 서비스의 메시지는 /etc/syslog.conf에 쓰여 있는 문서 파일에서 볼 수 있는데 syslogd 데몬을 실행하는 원격 머신에서 받을 수도 있다.
리눅스 syslog 인터페이스를 사용하는 방법은 매우 간단하다. 세 가지 함수가
/usr/include/syslog.h에 정의되어 있다(syslog.3 매뉴얼 페이지를 참조).
void openlog(char *ident, int option, int facility);
void syslog(int priority, char *format, ...);
void closelog(void);
openlog()는 시스템 기록기에 접속을 생성한다. ident 문자열은 각각의 기록되는 메시지에 붙게 되는데 보통 데몬의 이름이 된다. option 인자는 에러가 발생한 경우 콘솔에 기록할지, 표준 에러에 기록할지, 프로세스 번호를 기록할지를 지정한다. facility 인자는 메시지를 기록하는 프로그램이나 데몬의 형태를 분류하는데 기본값은 LOG_USER이다.
syslog() 호출은 실제로 기록을 하게 된다. format의 값과 변수 인자는 printf() 에서 볼 수 있는 것과 비슷한데 예외가 있다면 %m은 현재 errno값에 해당하는 에러 메시지로 대치된다는 것이다. priority 인자는 메시지가 기록될 때 메시지의 형태와 중요도를 가리킨다.
시스템 기록기와의 접속을 끊고 관련된 파일 디스크립터와 소켓을 닫으려면 closel og()를 사용한다. openlog()와 closelog()를 사용하는 것은 선택사항이다.
이 함수에 대한 더 자세한 정보는 syslog(3) 매뉴얼 페이지에서 볼 수 있다.
Ivan Griffin씨는 아일랜드 Limerick 대학에서 전자, 컴퓨터 공학과의 대학원생
이다.
그의 관심분야는 C++/자바, WWW, ATM, UL 컴퓨터 모임 (http://www.csn.ul.ie/)와 물론 리눅스도 포함된다 (http://www.trc.ul.ie/~griffini/linux.html). 그의 전자우편 주소는
ivan.griffin@ul.ie 이다.
John Nelson 박사는 Limerick 컴퓨터 공학과의 교수이다. 그의 관심사는 이동 통신, 지능형 네트워크, 소프트웨어 공학과 VLSI 설계 등이다. 그의 전자우편 주소는 john.nelson@ul.ie이다.