컴퓨터 공부/네트워크 프로그래밍

[TCP/IP 소켓 프로그래밍] 10. 멀티태스킹 기반의 서버구현

려리군 2009. 7. 27. 17:20

10-1 다중 접속 서버의 구현 방법들

※ 클라이언트의 (여러명)다중 접속을 허용(concurrent server).

리눅스 기반의 다중 접속 서버 구현 방법들

1. 프로세스 생성을 통한 멀티태스킹(Multitasking) 서버의 구현

2. select 함수에 의한 멀티플렉싱(Multiplexing) 서버의 구현

3. 쓰레드 기반으로 하는 멀티쓰레딩(Multithreading) 서버의 구현


10-2. 프로세스의 생성

프로세스에 대한 이해

1. 실행되고 있는 프로그램의 기본 단위

2. 생성된 프로세스는 운영체제에 의해 할당된 고유한 ID를 가진다.

3. 하나의 프로그램 내에서 여러 개의 프로세스가 동시에 실행 될 수 있다.


ps -u : 어떤 프로세스가 수행되고 있는 지 보여준다. (shell 명령)

pid : 프로세스 아이디


fork 함수 호출을 통한 프로세스 생성


#include<sys/types.h>

#include<unistd.h>


pid_t fork(void);

성공시 프로세스 ID(자식프로세스면 0), 실패시 -1


프로세스 생성 예제

소스내용

a=10;

(... 중략 ...)

pid = fork();

// fork()를 호출하자마자 메모리 상태.

(... 중략...)

printf("fork 성공, 프로세스 id : %d\n",pid);

if(pid==0)

    data +=10;

else

    data -=10;

printf("data : %d",pid);

(...중략...)


10-3 프로세스&좀비(Zombie) 프로세스

좀비 프로세스 : 프로세스 종료후 메모리 상에서 사라지지 않는 프로세스

첫번째 그림

자식 프로세스는 부모 프로세스가 (종료)리턴 값을 받을 때까지 메모리 상에서 사라지지 않는다.

(죽지 않는다.)


두번째 그림

자식 프로세스가 (종료)리턴해 준 값을 커널이 부모 프로세스에게 주고 자식 프로세스를 종료한다.

부모 프로세스는 자식 프로세스의 (종료)리턴 값을 달라고 요구해야 좀비 프로세스가 생성되지 않는다.


※ int main()에서 return 0;하는 이유

프로그램의 main은 프로세스 생성을 요구한다.

리턴의 의미는 커널이 제대로 종료했는 지 알기 위해 필요한 값이다.


좀비 프로세스의 생성 이유.

자식 프로세스는 부모 프로세스에게 실행 결과에 대한 값을 반환해야 한다.

부모 프로세스가 반환받지 않으면 자식 프로세스는 좀비 프로세스가 된다.


wait함수의 사용

#include<sys/types.h>

#include<sys/wait.h>

pid_t wait(int *status);

자식 프로세스를 기다린다. 자식 프로세스가 종료하지 않으면 무한 대기할 수도 있다.

리턴 : 성공시 자식 프로세스 ID, 실패시 -1

status : 다음 매크로 함수 이용

WIFEXITED(status) : 정상 종료시 0을 반환

WEXITSTATUS(status) : 종료시 return하거나 exit함수의 인자로 넘겨진 값을 반환한다.


좀비 프로세스 소멸

소멸 방법 : 부모 프로세스에서 자식 프로세스의 반환 값을 요구한다.


waitpid함수의 사용

#include<sys/types.h>

#include<sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);

참고주소 : http://linux.die.net/man/2/waitpid

리턴 : 성공시 종료된 자식 프로세스 ID, 실패시 -1

option이 WNOHANG이고 상태가 변한 프로세스가 없으면 0을 리턴.

pid 

-1이면 임의의 자식 프로세스가 종료하기를 기다리게 된다.

0이면 호출한 프로세스의 아이디와 같은 그룹일 경우.

0이상이면 기다리길 원하는 자식 프로세스의 ID. 

status : wait 함수의 status와 동일.

WNOHANG : 종료한 자식 프로세스가 없는 경우 대기 상태로 들어가지 않고 바로 리턴.


10-4. 시그널(Signal) 핸들링 & 좀비(Zombie) 프로세스

어떻게 효율적으로 좀비 프로세스를 처리할까?

부모 프로세스 입장에서 좀비(자식) 프로세스가 발생하는 시점을 모르기 때문에 (시스템은 알고 있음.)

운영체제가 시그널을 보내 부모 프로세스가 그 시그널을 읽어서 (시그널 핸들러를 통해) 자식 프로세스를 처리.


시그널

시스템 내의 특정상황 발생을 알리기 위해서 커널이 전달하는 신호. (운영체제에 의해 약속된 상수.)

시그널 핸들러

적절한 처리를 해 주는 함수

시그널 핸들링

시그널이 발생함에 따라 이에 대한 적절한 처리를 해 주는 것.


1. 자식 프로세스가 종료되었을 때(특정 상황) 부모 프로세스에게 시그널을 보냄.

2, 부모 프로세스는 시그널을 받으면 시그널 핸들러를 실행


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

signal함수

#include<signal.h>

void (*signal(int signum, void (*func)(int)))(int);

시그널(signum)과 시그널 핸들러(func)를 연결시켜주는 기능.

(해당 시그널 번호를 받으면 시그널 핸들러 함수가 실행된다.)

리턴이 void (*)(int); 함수 포인터 타입

리턴 : 오류시 SIG_ERR

signum

 시그널발생상황 
 SIGALRM시간을 예약해 놓고 그 시간이 되었을 경우 발생한다. 
 SIGINTCtrl-C를 누를 경우 인터럽트 발생 
 SIGCHLD자식 프로세스가 종료된 경우 발생. 


※ 시그널 핸들러에서 signal함수를 넣는 이유

운영체제가 시그널을 등록하면 딱 한 번만 signal함수를 호출하는 경우가 있기 때문에

※ signal로 시그널을 등록하지 않으면 운영체제에서 기본적으로 정의한 signal 핸들러가 호출된다.


sigaction함수

#include<signal.h>

시그널(signum)과 시그널 핸들러(func)를 연결시켜주는 기능. 이 함수를 사용하는 것을 추천.

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);


struct sigaction

{

    void (*sa_handler)(int);

    sigset_t sa_mask;    

    int sa_flags;

};


sa_mask

동시에 signal이 발생해도 처리할 수 있도록 한다. 

pending된 signal을 block, unblock시키는 용도로 사용.


sigemptyset : 시그널 마스크 비트(sa_mask)를 0으로 설정.


10-5. fork 함수를 이용한 다중 접속 서버의 구현


1. 클라이언트가 서버에게 연결 요청

2. 서버 프로세스가 연결 요청을 받을 때마다 자식 프로세스 생성 -> 자식 프로세스는 부모 프로세스가 전달한 디스크립터(client_sock)을 받는다.

3. 클라이언트와 자식 프로세스와 통신

※ 소켓은 운영체제가 관리하기 때문에 fork를 해도 복사되지 않는다. 파일 디스크립터는 복사가 된다.


파일 디스크립터의 복사

하나의 소켓에 대한 파일 디스크립터가 둘 이상 존재하는 경우, 모든 파일 디스크립터를 종료해 줘야 해당 소켓이 종료된다.

※ 커널에서 소켓은 참조카운트를 가지고 있을 듯...


부모 프로세스는 클라이언트 소켓이 필요없고 자식 프로세스는 서버 소켓이 필요없으므로 복제된 후 미리 삭제해 준다.


10-6. TCP 입출력 루틴(Routine) 분할하기

에코 클라이언트의 경우 적용 가능(그냥 하나의 모델로서... 소스가 복잡해 지는 단점이 있다.)

※ 송수신이 잦은 프로그램에 한해 효율성이 증가할 수 있다.


입력과 출력을 실행하는 루틴을 부모-자식 프로세스로 분리한다.

=> 입출력이 독립되어 다음과 같은 통신이 가능하다.