본문 바로가기

My Study/Programming&Theory

멀티태스킹 기반 데몬 서버 ( linux )

 CTF 해킹 대회를 보고 난 후 문제들이 전부 데몬으로 만들어진다는 것을 알았습니다..
나중에 문제도 만들어봐야 하므로 기본적인 멀티태스킹 기반으로 돌아가는 데몬 서버를 만들어 봤습니다.
환경은 linux입니다.

간략히 코드 구성 형태를 적어보겠습니다.
1. 프로세스 데몬화
2. 기본 소켓 통신을 위한 초기화 ( socket, bind, listen )
3. 멀티 태스킹 서버 만들기 ( fork )
4. fork로 인해 생긴 자식 프로세스 정상 종료 ( 좀비 프로세스 방지, sigaction, waitpid )
위 4가지를 구현해 봤습니다.

1. 프로세스 데몬화
먼저 데몬이라는 것은 프로세스입니다. 
즉, 시스템 백그라운드에서 돌아가고있는 프로세스이죠. 하지만 일반 프로세스와는 몇가지 다른 점이 있습니다.

첫째, 터미널 장치를 가지고 있지 않습니다. ( TTY가 ? 로 나타납니다. )
둘째, ppid가 1로 셋팅되어 있고 session id 또한 자신의 pgid와 같습니다.

먼저 시스템에서 데몬형태로 돌아가고 있는 프로세스를 봐보도록 하겠습니다.

위 몇몇 프로세스들도 데몬이고 빨간색으로 네모친 프로세스는 제 프로세스를 데몬화 시킨 것입니다.
보시면 ppid는 1로 되어있고 pid, pgid, sid 전부 같고 TTY는 ?로 있는 것을 볼수 있습니다.

데몬화 시키는 방법입니다.
먼저 fork로 자식 프로세스를 하나 생성시킵니다.
그리고 부모 프로세스는 먼저 죽어버립니다.

간단히 코드로 구현하면
pid = fork();
if( pid != 0 )
     exit(0);
     .
     .        //코드 진행
위와 같이 됩니다. 자식 프로세스는 fork시 리턴 값이 0이므로 종료가 되지 않습니다.
그러면 원래 자식 프로세스의 ppid는 부모 프로세스의 pid값을 지니고 있는데
부모가 죽어버렸으니 자식을 누군가가 대려가야합니다. (????!!!!!)
바로 그 분이 pid 값 1을 지니고 있는 init 프로세스 입니다.
init 프로세스가 하는 일은 이렇습니다.
1. 파일 시스템의 구조를 검사
2. 파일 시스템을 마운트
3. 서버 데몬을 띄움
4. 사용자 로그인을 기다림
5. 사용자를 위한 쉘을 띄우는 일
아무튼 init 프로세스에게 자식이 양도되면 자식 프로세스의 ppid는 1이 되게 됩니다.

일단 데몬이 되기 위한 조건 하나를 만족했군요.

그 다음은 TTY를 없애는 것입니다.
이것은 간단하게 0,1,2 파일 디스크립터를 닫아버리면 됩니다.
close(0);
close(1);
close(2);
이런식으로 말이죠. 
데몬 특성상 불필요한 디스크립터는 전부 제거해 주어야합니다. ( 윈도우로 말하면 핸들... )

그 다음은 새로운 세션의 리더가 되는 것입니다.
사용하는 함수는 setsid입니다.
해당 함수를 사용할 경우
프로세스가 프로세스 그룹의 리더가 아니라면 새로운 세션을 생성하고 해당 세션에 대한 그룹의 리더가 됩니다.
그리고 tty를 제어할 수 없게 됩니다.
이렇게 setsid를 호출해서 세션을 생성하고 그룹의 리더가 되었다면 sid와 gid는 pid와 동일하게 됩니다.
그리고 해당 프로세스에서 생성되는 모든 프로세스들은 이 sid와 gid를 가지게 됩니다.

세션이라는 것은 그룹들의 집합이라고 생각하시면 됩니다.
코드는
setsid();
이렇게만 해주면 끝입니다. ( 데몬은 여기서 완성.. )

그리고 마지막으로 작업디렉토리를 root 디렉토리로 변경해주어야 합니다.
해당 데몬의 작업 디렉토리는 데몬을 실행 시켰던 디렉토리입니다. 만약에 해당 디렉토리가 속한 파일 시스템을
언마운트 하려 하면 OS에서는 파일 시스템이 사용되고 있다면서 언마운트 하지 못할 것입니다.
그렇기 때문에 데몬은 작업 디렉토리를 root 디렉토리로 변경해주는 것이 좋습니다. ( "/"은 언마운트 될수 없음 )

이렇게 해서 데몬화는 전부 끝났습니다.
부가적으로 더 해주면 좋을 작업이 로그아웃 같은 행위를 했을 때 데몬이 꺼지지 않고 그대로 작동하도록 하는 것입니다.
간단하게 sigaction함수를 사용해 SIGHUP시그널을 무시해버리도록 하면 됩니다.

2. 기본 소켓 통신을 위한 초기화 ( socket, bind, listen )
단순하게 socket으로 소켓을 하나 만들고 bind로 해당 소켓에 주소와 포트를 할당해주고 listen으로 대기큐를 만들기만 하면 됩니다. 소켓 프로그래밍의 가장 기본적인 부분이므로 넘어가겠습니다.

3. 멀티 태스킹 서버 만들기 ( fork )
이제 데몬 서버가 돌아가면 여러 사용자들이 해당 데몬에 접속을 할 것입니다.
이 때 한명이 모든 할일을 마쳐야 다음 사람이 그 할일을 하고.. 또 그 할일을 다 마쳐야 그 다음사람이 할일을 하고.....
이러한 형태가 되어서는 안됩니다. 
그렇기 때문에 여러 사람이 동시에 같은 일을 들어와서 할 수 있도록 서버를 만들어야합니다.

윈도우에서는 스레드를 사용해서 구현해 보았는데 여기서는 fork라는 함수를 사용해서 구현을 해보겠습니다.
제가 아는 선에선 fork함수 같은 함수는 윈도우에 없는 걸로 알고 있습니다.  아~ 슈도코드로 만들수는 있겠군요.

다시 본론으로 돌아와서 일단 코드를 봐보겠습니다. 

계속적으로 accept를 해주어야하므로 일단 while문으로 묶었습니다.
설명을 해보면
accept가 작동해서 클라이언트와 연결이 됬습니다. 이 때 fork함수를 사용해 자식 프로세스를 하나 만들고 
부모 프로세스는 그냥 연결된 클라이언의 소켓 핸들을 제거하고 다시 accept로 돌아와버립니다.
그리고 자식 프로세스에서는 연결된 클라이언트와 대화를 하면 됩니다.
( fork를 했을 경우 클라이언트 소켓을 가리키는 프로세스는 2개가 됨.. 클라이언트는 그대로 하나..! )

이 과정이 전부입니다.

4. fork로 인해 생긴 자식 프로세스 정상 종료 ( 좀비 프로세스 방지, sigaction, waitpid )
위 코드를 다시 보면 자식프로세스가 종료할 때 exit를 사용하고 있습니다. 그러면 그냥 자식 프로세스는 종료가 될까요?
아닙니다. 자식 프로세스의 리턴 값이 부모 프로세스에게로 넘어와야 그제서야 종료가 이루어 집니다. 종료가 이루어지기 전까진 좀비 프로세스 상태로 있습니다. ( 그냥 좀비 =_=~~ 허..~~~ 리소스만 잡아먹는...!! )
단순히 exit만 썼다고 해서 자식 프로세스의 리턴 값이 부모 프로세스에게 넘어오질 않습니다. 만약 이 상태로 부모 프로세스에선 자식 프로세스의 리턴 값을 받지 않고 계속 accept만 한다면 좀비 프로세스는 계속적으로 늘어나고 결국 시스템 상의 모든 리소스가 다 차버리게 됩니다. 더이상 프로세스 생성을 못하니 서버로서의 역할도 끝나는 것이지요.

간단하게 자식 프로세스의 리턴 값을 받는 함수 하나만 사용해 주면 해결됩니다.
waitpid 함수이죠. ( wait 함수는 blocking처리를 할 수 없으므로... )

waitpid(-1,&status,WNOHANG)
위와 같이 써주면 됩니다.
-1 이라는 것은 임의의 자식 프로세스를 뜻하는 것이고 
WNOHANG은 종료할 자식 프로세스가 없어도 기다리지 말고 즉시 리턴하라는 뜻입니다.

뭔가 이상할 것입니다. 그러면 하나의 자식 프로세스가 끝날 때 까지 기다려야된다는 말인가??
이건 멀티 태스킹이 아닙니다. 그렇기 때문에 시그널 핸들링을 사용해 주어야 합니다.

작동 형태는 이렇습니다.
1. 부모 프로세스는 계속적을 할일을 합니다.
2. 자식 프로세스가 종료를 했습니다.
3. OS는 부모 프로세스에게 알려줍니다. " 자식 프로세스가 종료했어~~ "
4. 부모 프로세스는 하던일을 잠시 멈추고 자식 프로세스의 리턴 값을 가져와 자식 프로세스를 정상 종료 시킵니다.
이때 OS가 부모 프로세스에게 자식 프로세스가 종료 됬다고 알려주는 행위가 시그널이고 부모 프로세스가 자식 프로세스를 종료 시키는 부분이 핸들링 입니다. 즉, 특정 시그널이 발생하면 특정 함수가 작동되는 것이죠.

사용되는 함수는 sigaction입니다.
struct sigaction act;

act.sa_handler = child_end;        //시그널 발생시 호출될 함수
sigemptyset(&act.sa_mask);
act.sa_flags = SA_RESTART;      // 시스템 호출 재시작 하도록...
sigaction(SIGCHLD,&act,0);     //자식프로세스 종료 시그널 핸들링

위와 같이 사용하면 됩니다.
child_end 함수안에는 waitpid가 있습니다..~~

완성이군요. 직접 구현해 보시는 것도 재미있습니다 ^^