2014년 12월 14일 일요일

TCP/IP 소켓 프로그래밍 10강

10-1 : 프로세스의 이해와 활용
10강은 리눅스에서 할 수 있는 멀티 프로세스 기반의 서버 구현이다.
일단 프로세스란 "메모리 공간을 차지한 상태에서 실행 중인 프로그램"이다.
단순히 하드디스크에 저장되어 있는 프로그램은 그저 파일에 불과하다
프로그램을 실행하면 하드->메모리로 올리는 과정이 로딩이고,
메모리로 올라오면 OS로부터 프로세스 ID를 부여 받는다.
이것을 Process ID = PID라고 불린다.
리눅스 상에서 pa au 명령어를 입력하면 현재 실행중인 프로세스를 확인할 수 있다.
PID는 2 이상의 정수 형태로 받는다.(1은 os에서 사용하는 프로세스에 부여된다)

그럼 이 프로세스를 복사하는 방법을 알아보자.
#include <unistd.h>
pid_t fork(void);

fork함수는 프로세스의 복사본을 생성한다. 이것을 복사하는 이유는 동일한 프로세서를 분리해서 실행시키기 위해서다.
하나는 입력을, 하나는 출력을 담당시켜서 부하를 줄이려는 것이다.
원본 프로세서는 부모 프로세서로, 복사된 프로세서는 자식 프로세스로 불리운다.
소스를 보자.

#include <stdio.h>
#include <unistd.h>
int gval=10;

int main(int argc, char *argv[])
{
pid_t pid;
int lval=20;
gval++, lval+=5;
//자식 프로세스를 생성하고 있다. 부모 프로세스의 pid에는
//자식 프로세스의 id가 저장 되며, 자식 프로세스의 pid에는 0이 저장된다.
pid=fork();


if(pid==0) // 자식 프로세스가 실행한다.
gval+=2, lval+=2;
else // 부모 프로세스가 실행한다.
gval-=2, lval-=2;

if(pid==0) //자식 프로세스가 실행한다.
printf("Child Proc: [%d, %d] \n", gval, lval);
else //부모 프로세스가 실행한다
printf("Parent Proc: [%d, %d] \n", gval, lval);
return 0;
}

10-2 : 프로세스 & 좀비 프로세스
프로세스의 복사는 위와 같고, 이제는 해제하는 법을 알아야 하는데 주의할 점이 있다.
잘못 해제하면 좀비 프로세스가 되어서 시스템의 리소스를 잡아 먹으며 죽지 않는 경우가 생긴다. (농담이 아니고 진짜 좀비 프로세서로 불리운다-_-;)

좀비 프로세스의 생성 이유

fork 함수로 생성된 자식 프로세스가 종료되는 상황은 2가지다.
-인자를 전달하면서 exit를 호출하는 경우
-main 함수에서 return 문을 실행하면서 값을 반환하는 경우

exit 함수로 전달되는 인자 값과 main함수의 return문에 의해 반환되는 값 모두
운영체제로 전달되는데, 운영체제는 이 값이 자식 프로세스를 생성한
부모 프로세스에게 전달될 때까지 자식 프로세스를 소멸시키지 않는다.
이 상황이 바로 좀비 프로세스다.

"해당 자식 프로세스를 생성한 부모 프로세스에게 exit함수의 인자 값이나 return문의 반환 값이 전달 되어야" 좀비 프로세스가 소멸 된다.

좀비 프로세스의 소멸 1 : wait 함수의 사용

#include <sys/with.h>
pid_t wait(int* statloc);

위 함수를 호출했을 때 이미 종료된 자식 프로세스가 있다면, 자식 프로세스가 종료되면서 전달한 값이 매게변수로 전달된 주소의 변수에 저장된다.
그런데 이 변수에 저장되는 값에는 자식 프로세스가 존료되면서 전달한 값 이외에도 다른 정보가 함께 포함되어 있으니, 다음 매크로 함수를 통해서 값의 분리 과정을 거쳐야 한다.

-WIFEXITED : 자식 프로세스가 정상 종료한 경우 (TRUE)를 반환한다.
-WEXITSTATUS : 자식 프로세스의 전달 값을 반환한다.

예제를 보자.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
int status;
pid_t pid=fork();

if(pid==0)
{
return 3;  
}
else
{
printf("Child PID: %d \n", pid);
pid=fork();
if(pid==0)
{
exit(7);
}
else
{
printf("Child PID: %d \n", pid);
//wait 함수를 호출하고 이로 인해 종료된 프로세스 관련 정보는 status에 담기게 되고,
//해당 정보의 프로세스는 완전히 소멸된다.
wait(&status);
//WIFEXITED를 통해 자식 프로세스의 정상종료 여부를 확인한다.
//그리고 정상 종료인 경우에 한해서 
//WEXITSTATUS 함수를 호출해서 자식 프로세스가 전달한 값을 출력한다.
if(WIFEXITED(status))
printf("Child send one: %d \n", WEXITSTATUS(status));
//앞서 생성한 자식 프로세스가 두 개이므로 또 한번의 wait 함수호출과 매크로 함수의 호출한다.
wait(&status);
if(WIFEXITED(status))
printf("Child send two: %d \n", WEXITSTATUS(status));
//부모 프로세스의 종료를 멈추기 위해서 삽입한 코드
sleep(30);     // Sleep 30 sec.
}
}
return 0;
}

wait 함수는 호출된 시점에서 종료된 자식 프로세스가 없다면 임의의 자식 프로세스가 종료될 때까지 블로킹 상태에 놓인다는 특징이 있다. 때문에 함수의 호출에 주의해야 한다.

좀비 프로세스의 소멸2: waitpid 함수의 사용

wait함수의 블로킹이 문제가 된다면 waitpid 함수의 호출을 사용하면 된다.

#include <sys/wait.h>
pid_t waitpid(pid_t pid, int* statloc, int options);

-pid :  종료를 확인하고자는 자식 프로세스의 id전달, 이를 대신해서 -1을 전달하면 wait함수와 마찬가지로 임의의 자식 프로세스가 종료되기를 기다린다.
-statloc : wait 함수의 매개변수 statloc와 동일한 의미로 사용된다.
-options : 헤더파일 sys/wait.h에 선언된 상수 WNOHANG을 인자로 전달, 종료된 자식 프로세스가 존재하지도 않아도 블로킹 상태에 있지 않고, 0을 반환하면서 함수를 빠져나온다.

예제를 보자.

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
int status;
pid_t pid=fork();

if(pid==0)
{
sleep(15); //자식 프로세스의 종료를 늦추기 위해 호출
return 24;  
}
else
{
//while문 내에서 waitpid를 호출하고 있다. 세 번째 인자로 WNOHANG을 전달하였으니 종료된 자식 프로세스가 없으면 0을 반환한다.
while(!waitpid(-1, &status, WNOHANG))
{
sleep(3);
puts("sleep 3sec.");
}

if(WIFEXITED(status))
printf("Child send %d \n", WEXITSTATUS(status));
}
return 0;
}

10-3 시그널 핸들링

위의 예제의 문제는 자식 프로세스가 종료될 때까지 waitpid 함수를 계속 호출해야 한다.
이건 낭비다. 이 문제를 해결하는 법을 보자.
어차피 자식 프로세스가 종료 되었다는 것을 아는 건 운영체제다.
그럼 운영체제가 부모 프로세스에게 자식 프로세스가 종료되었다는 것을 알려준다면
부모 프로세스가 자식 프로세스를 죽일 것이다.(부모가 자식을 킬....)

이러한 프로그램 구현을 위해 '시그널 핸들링'이라는 것이 존재한다.
시그널은 특정 상황이 발생했음을 알리기 위해 운영체제가 프로세스에게 전달하는 메세지다.

시그널을 운영체제에게 등록하는 함수를 보자
#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);
-시그널 발생시 호출되도록 이전에 등록된 함수의 포인터 반환

위 함수는 반환형이 함수 포인터라 선언이 좀 다르다.
(void*) signal(int signo, (void*)func(int)) (int);
위와 같이 표현해도 될 것이다.
-함수 이름: signal
-매개변수 선언 : int sinno, (void*)func(int)
-반환형 : 매개변수형이 int이고 반환형이 void인 함수 포인터

signal 함수를 통해서 등록가능한 특정 상황과 그 상황에 할당된 상수를 몇개 보자
-SIGALRM : alarm 함수 호출을 통해서 등록된 시간이 된 상황
-SIGINT : 컨트롤 + C가 입력된 상황
-SIGCHLD : 자식 프로세스가 종료된 상황

//alarm함수 호출을 통해서 등록된 시간이 지나면 timeout 함수를 호출해달라.
signal(SIGALRM, timeout);
//컨트롤 +C가 입렫되면 keycontrol 함수를 호출해달라.
singnal(SIGINT, keycontrol);

alarm 함수
#include <unistd.h>
unsigned int alarm(unsigned int seconds);

위 함수를 호출하면서 양의 정수를 인자로 전달하면, 전달한 수에 해당하는 시간이 지나서 SINALRM 시그널이 발생한다. 그리고 0을 전달하면 이전에 설정된 시그널 발생의 예약이 취소된다.

sigaction 함수를 이용한 시그널 핸들링

#include <singnal.h>
int sigaction(int signo, const struct singaction * act, struct sigaction * oldact);
-sinno : signal 함수와 마찬가지로 시그널의 정보를 인자로 전달
-act : 첫번째 인자로 전달된 상수에 해당하는 시그널 발생시 호출될 함수의 정보 전달
-oldact : 이전에 등록되었던 시그널 핸들러의 함수 포인터를 얻는데 사용되는 인자, 필요 없으면 0 전달

위 함수들을 이용해서 다중 접속 서버를 구현한 소스가 있는데
여기에 기재하기엔 길다 ㅡ.ㅡ;
그래서 역시 소스를 다운 받아서 직접 실행해보는 것이 좋다.
소스 다운 받기

희안하게 책만 읽었을 때는 이해가 안가고, 소스코드를 실행시켜 보면 조금 이해가 가고,
블로그에 정리해서 올리면 더 많이 이해가 간다.
이 블로그에 기재하는 이유가 나 스스로가 이해력을 높이기 위한 것도 있다.
여기에 동영상 강의까지 들으면 완전(?) 이해가 될 듯하다.
아직 동영상 강의를 10강까지는 듣지 못했다.
프로그래밍 공부는 직접 해보는 것이 가장 좋은 듯하다.
그래서 블로그에 기재할 때는 중요 함수만 나열하려고 한다.
전에 쓴 것을 보니 내가 봐도 난잡하다 -_-;
기억나지 않을 때 찾아보는 용도로나 할려고 한다.

댓글 없음:

댓글 쓰기