2011년7월19일_fork( )를 사용한 멀티프로세스 TCP/IP 서버와 클라이언트간 양방향통신시 문제점 ③ 해결책


어제 실습했던 코드…

http://sumanaki.tistory.com/133
fork( )를 사용한 멀티테스킹 TCP/IP 서버와 클라이언트간 양방향통신시 문제점 ③

에서, 서버와 클라이언트를 종료할 때 각 프로그램의 자식 프로세스가 고아로 남는 문제점이 있었다.
오늘은 이 문제를 해결해보자.

우선 어제 작성한 소스코드에서 부모와 자식을 바꾸자.

   1:  if(0 < fork_ret)
   2:  {
   3:      //부모프로세스가 하는 일
   4:  }
   5:  else if(0 == fork_ret)
   6:  {
   7:      //자식프로세스가 하는 일
   8:  }



● talk_server.c

   1:  // 프로세스가 남는 문제가 해결된 talk_server.c
   2:  #include <stdio.h>
   3:  #include <string.h>
   4:  #include <stdlib.h>
   5:  #include <sys/types.h>
   6:  #include <signal.h>
   7:  #include <sys/socket.h>
   8:  #include <sys/wait.h>
   9:  #include <netinet/in.h>
  10:  #include <arpa/inet.h>
  11:  #include <unistd.h>
  12:   
  13:  #define    MAXLINE    512
  14:   
  15:  void z_handler();        //시그널 처리함수
  16:   
  17:  char *escapechar = "exit"; //종료문자열
  18:   
  19:  int main(int argc, char *argv[])
  20:  {
  21:      int server_sock;
  22:      int client_sock;
  23:      int clntlen;
  24:      int num;
  25:      char sendline[MAXLINE];
  26:      char buffer[MAXLINE];
  27:      char recvline[MAXLINE];
  28:      int size;
  29:      pid_t fork_ret;
  30:      struct sockaddr_in client_addr;
  31:      struct sockaddr_in server_addr;
  32:      // signal사용하기 위해 추가된 변수들
  33:      int state;
  34:      struct sigaction act;
  35:   
  36:      if(argc != 2)
  37:      {
  38:          printf("Usage : %s PORT \n", argv[0]);
  39:          exit(0);
  40:      }
  41:      
  42:      // signal설정
  43:      act.sa_handler = z_handler;
  44:      sigemptyset(&act.sa_mask);
  45:      act.sa_flags = 0;
  46:   
  47:      state = sigaction(SIGCHLD, &act, 0);    //자식프로세스 종료시 발생하는 시그널 SIGCHLD이용
  48:   
  49:      if(0 != state)
  50:      {
  51:          fprintf(stderr, "sigaction() error. \n");
  52:          exit(1);
  53:      }
  54:   
  55:      // 소켓 생성
  56:      if((server_sock = socket(PF_INET, SOCK_STREAM, 0)) < 0)
  57:      {
  58:          printf("Server: can't open stream socket. \n");
  59:          exit(0);
  60:      }
  61:   
  62:      // 소켓주소 구조체에 주소 세팅
  63:      bzero((char *)&server_addr, sizeof(server_addr));    //소켓주소 구조체 초기화
  64:      server_addr.sin_family = AF_INET;
  65:      server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  66:      server_addr.sin_port = htons(atoi(argv[1]));
  67:   
  68:      // 소켓에 서버 주소 연결
  69:      if(bind(server_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
  70:      {
  71:          printf("Server: can't bind local address. \n");
  72:          exit(0);
  73:      }
  74:   
  75:      printf("Server started. \nWaiting for client..\n");
  76:      listen(server_sock, 1);
  77:   
  78:      // 클라이언트의 연결요청 수락
  79:      clntlen = sizeof(client_addr);
  80:      if((client_sock = accept(server_sock, (struct sockaddr *)&client_addr, &clntlen)) < 0)
  81:      {
  82:          printf("Server: failed in acceptiong. \n");
  83:          exit(0);
  84:      }
  85:   
  86:      fork_ret = fork();
  87:      
  88:      if(0 < fork_ret)
  89:      {
  90:          // 부모 프로세스는 키보드 입력을 클라이언트로 송신
  91:          while(fgets(sendline, MAXLINE, stdin) != NULL)
  92:          {
  93:              strcpy(buffer, "수만 : ");
  94:              strcat(buffer, sendline);
  95:              size = strlen(buffer);
  96:              if(write(client_sock, buffer, strlen(buffer)) != size)
  97:              {
  98:                  printf("Error in write. \n");
  99:              }
 100:   
 101:              if(strstr(sendline, escapechar) != NULL)    //종료문자열 입력시 처리
 102:              {
 103:                  printf("Good bye. \n");
 104:                  close(client_sock);
 105:                  break;                
 106:              }
 107:          }
 108:      }
 109:      else if(0 == fork_ret)
 110:      {
 111:          // 자식 프로세스는 클라이언트로부터 수신된 메시지를 화면에 출력
 112:          while(1)
 113:          {
 114:              if((size = read(client_sock, recvline, MAXLINE)) < 0)
 115:              {
 116:                  printf("Error if read. \n");
 117:                  close(client_sock);
 118:                  exit(0);
 119:              }
 120:   
 121:              recvline[size] = '\0';
 122:   
 123:              if(strstr(recvline, escapechar) != NULL)    //종료문자열 입력시 처리
 124:              {
 125:                  write(client_sock, escapechar, sizeof(escapechar));
 126:                  break;
 127:              }
 128:              
 129:              printf("%s", recvline);        //화면 출력
 130:          }
 131:      }
 132:   
 133:      close(server_sock);
 134:      close(client_sock);
 135:   
 136:      return 0;
 137:  }
 138:   
 139:  //시그널 처리함수
 140:  void z_handler()        
 141:  {
 142:      int state;
 143:   
 144:      waitpid(-1, &state, WNOHANG);
 145:      exit(0);        //자식이 소멸되며 부모도 같이 종료.
 146:   
 147:      return ;
 148:  }




● talk_client.c

   1:  // 프로세스가 남는 문제가 해결된 talk_client.c
   2:  #include <stdio.h>
   3:  #include <string.h>
   4:  #include <stdlib.h>
   5:  #include <sys/types.h>
   6:  #include <signal.h>
   7:  #include <sys/socket.h>
   8:  #include <netinet/in.h>
   9:  #include <arpa/inet.h>
  10:  #include <sys/wait.h>
  11:   
  12:  #define    MAXLINE    1024
  13:   
  14:  void z_handler();        //시그널 처리함수
  15:   
  16:  char *escapechar = "exit"; //종료문자열
  17:   
  18:  int main(int argc, char *argv[])
  19:  {
  20:      char line[MAXLINE];
  21:      char sendline[MAXLINE];
  22:      char recvline[MAXLINE + 1];
  23:      char buffer[MAXLINE];
  24:      int n;
  25:      int size;
  26:      int comp;
  27:      int addr_size;
  28:      pid_t fork_ret;
  29:      static int s;
  30:      static struct sockaddr_in server_addr;
  31:      // signal사용하기 위해 추가된 변수들
  32:      int state;
  33:      struct sigaction act;
  34:      
  35:      if(argc != 3)
  36:      {
  37:          printf("Usage : ./%s serverIP serverPORT \n", argv[0]);
  38:          exit(0);
  39:      }
  40:   
  41:      // signal설정
  42:      act.sa_handler = z_handler;
  43:      sigemptyset(&act.sa_mask);
  44:      act.sa_flags = 0;
  45:   
  46:      state = sigaction(SIGCHLD, &act, 0);    //자식프로세스 종료시 발생하는 시그널 SIGCHLD이용
  47:   
  48:      if(0 != state)
  49:      {
  50:          fprintf(stderr, "sigaction() error. \n");
  51:          exit(1);
  52:      }
  53:      
  54:      // 소켓 생성
  55:      if((s = socket(PF_INET, SOCK_STREAM, 0)) < 0)
  56:      {
  57:          printf("Client: can't open stream socket. \n");
  58:          exit(0);
  59:      }
  60:   
  61:      // 소켓주소 구조체에 주소 세팅
  62:      bzero((char *)&server_addr, sizeof(server_addr));    //소켓주소 구조체 초기화
  63:      server_addr.sin_family = AF_INET;
  64:      server_addr.sin_addr.s_addr = inet_addr(argv[1]);
  65:      server_addr.sin_port = htons(atoi(argv[2]));
  66:   
  67:      // 서버에 연결 요청
  68:      if(connect(s, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
  69:      {
  70:          printf("Client: can't connect to server. \n");
  71:          exit(0);
  72:      }
  73:      
  74:      fork_ret = fork();
  75:      
  76:      if(0 < fork_ret)
  77:      {
  78:          // 자식 프로세스는 키보드 입력을 클라이언트로 송신
  79:          while(fgets(sendline, MAXLINE, stdin) != NULL)
  80:          {
  81:              strcpy(buffer, "수만: ");
  82:              strcat(buffer, sendline);
  83:              size = strlen(buffer);
  84:              if(write(s, buffer, strlen(buffer)) != size)
  85:              {
  86:                  printf("Error in write. \n");
  87:              }
  88:   
  89:              if(strstr(sendline, escapechar) != NULL)    //종료문자열 입력시 처리
  90:              {
  91:                  printf("Good bye. \n");
  92:                  close(s);
  93:                  exit(0);
  94:              }
  95:          }
  96:      }
  97:      else if(0 == fork_ret)
  98:      {
  99:          // 부모 프로세스는 클라이언트로부터 수신된 메시지를 화면에 출력
 100:          while(1)
 101:          {
 102:              if((size = read(s, recvline, MAXLINE)) < 0)
 103:              {
 104:                  printf("Error if read. \n");
 105:                  close(s);
 106:                  exit(0);
 107:              }
 108:   
 109:              recvline[size] = '\0';
 110:   
 111:              if(strstr(recvline, escapechar) != NULL)    //종료문자열 입력시 처리
 112:              {
 113:                  write(s, escapechar, sizeof(escapechar));
 114:                  break;
 115:              }
 116:              
 117:              printf("%s", recvline);        //화면 출력
 118:          }
 119:      }
 120:   
 121:      close(s);
 122:   
 123:      return 0;
 124:  }
 125:   
 126:  //시그널 처리함수
 127:  void z_handler()        
 128:  {
 129:      int state;
 130:   
 131:      waitpid(-1, &state, WNOHANG);
 132:      exit(0);
 133:   
 134:      return ;
 135:  }


<실행결과>

image
[스크린샷] 클라이언트측

image 
[스크린샷] 서버측

어제와 다르게 부모프로세스와 자식프로세스의 할 일을 바꾸니 상기와 같이 고아프로세스가 생기지 않고,
깔끔하게 동시에 접속이 끊어지고 종료됨을 알 수 있다.

image  

클라이언트측 또는 서버측에서 “exit”를 입력하여 부모프로세스가 “exit”를 상대편에 보낸 후 종료하면,
상대방 자식프로세스가 이스케이프 문자열 “exit”를 수신받아 바로 에코하고 종료되고,
자식프로세스 소멸시그널이 발생하여 부모프로세스도 종료된다. 이로써 수신받은 측은 모든 프로세스가 소멸되어 종료된다.
에코한 이스케이프 문자열 “exit”을 송신측에서 받는다.

image 

자식프로세스는 에코문자열을 받아 이스케이프처리를 하고, (에코)
종료된다. 그럼 자식프로세스가 죽었다는 시그널이 발생하고 부모프로세스는 자식프로세스의 리턴값처리를 하고 바로 종료된다.
이로써 송신한 측도 모든 프로세스가 소멸되어 종료되어 버린다.
잇힝~

그.런.데.
이 방식은 문제점이 있다. 송신측 부모프로세스가 먼저 종료되어 버려 잠시 그 자식프로세스는 고아상태가 된다는 것이다.
고아프로세스를 만들지 않으려면 이스케이프문자열처리 부분에서,

   1:  if(strstr(sendline, escapechar) != NULL)    //종료문자열 입력시 처리
   2:  {
   3:      printf("Good bye. \n");
   4:      close(client_sock);
   5:      break;                
   6:  }
   7:   
   8:  //이와 같이 수정해야 함.
   9:  if(strstr(sendline, escapechar) != NULL)    //종료문자열 입력시 처리
  10:  {
  11:      printf("Good bye. \n");
  12:      close(client_sock);
  13:      while(1);        //자식프로세스가 종료될 때까지 무한대기        
  14:  }

부모프로세스가 break하여 반복문을 빠져나와 종료되도록 하지 말고,
13행과 같이 자식프로세스가 종료될 때까지 무한대기하도록 하면 상대측에서 에코 “exit”가 도착할 때까지 기다린다.
               그럼 부모프로세스가 먼저 종료되는 일이 없으니 init에게 자식을 입양시키는 일은 없을 것이다. 하핳하



--------------------------------------------------------------------------------------------------------------------------------------------------------


● 선생님께선 서버와 클라이언트가 따로 종료되도록 바꾸라고 하셨다.
    자식프로세스가 송신을 하고 부모프로세스가 수신을 하도록 한다음 자식프로세스가 이스케이프 문자열 “exit”를 받았을 때,
    자식프로세스를 종료시키고, 시그널함수를 사용해 부모프로세스까지 종료시킨다는 시나리오이다.


● talk_server2.c

   1:  // 서버 클라이언트 각자 따로 종료되도록 수정한 코드 talk_server2.c
   2:  #include <stdio.h>
   3:  #include <string.h>
   4:  #include <stdlib.h>
   5:  #include <sys/types.h>
   6:  #include <signal.h>
   7:  #include <sys/socket.h>
   8:  #include <sys/wait.h>
   9:  #include <netinet/in.h>
  10:  #include <arpa/inet.h>
  11:  #include <unistd.h>
  12:   
  13:  #define    MAXLINE    512
  14:   
  15:  void z_handler();        //시그널 처리함수
  16:   
  17:  char *escapechar = "exit"; //종료문자열
  18:   
  19:  int main(int argc, char *argv[])
  20:  {
  21:      int server_sock;
  22:      int client_sock;
  23:      int clntlen;
  24:      int num;
  25:      char sendline[MAXLINE];
  26:      char buffer[MAXLINE];
  27:      char recvline[MAXLINE];
  28:      int size;
  29:      pid_t fork_ret;
  30:      struct sockaddr_in client_addr;
  31:      struct sockaddr_in server_addr;
  32:      // signal사용하기 위해 추가된 변수들
  33:      int state;
  34:      struct sigaction act;
  35:   
  36:      if(argc != 2)
  37:      {
  38:          printf("Usage : %s PORT \n", argv[0]);
  39:          exit(0);
  40:      }
  41:      
  42:      // signal설정
  43:      act.sa_handler = z_handler;
  44:      sigemptyset(&act.sa_mask);
  45:      act.sa_flags = 0;
  46:   
  47:      state = sigaction(SIGCHLD, &act, 0);    //자식프로세스 종료시 발생하는 시그널 SIGCHLD이용
  48:   
  49:      if(0 != state)
  50:      {
  51:          fprintf(stderr, "sigaction() error. \n");
  52:          exit(1);
  53:      }
  54:   
  55:      // 소켓 생성
  56:      if((server_sock = socket(PF_INET, SOCK_STREAM, 0)) < 0)
  57:      {
  58:          printf("Server: can't open stream socket. \n");
  59:          exit(0);
  60:      }
  61:   
  62:      // 소켓주소 구조체에 주소 세팅
  63:      bzero((char *)&server_addr, sizeof(server_addr));    //소켓주소 구조체 초기화
  64:      server_addr.sin_family = AF_INET;
  65:      server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  66:      server_addr.sin_port = htons(atoi(argv[1]));
  67:   
  68:      // 소켓에 서버 주소 연결
  69:      if(bind(server_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
  70:      {
  71:          printf("Server: can't bind local address. \n");
  72:          exit(0);
  73:      }
  74:   
  75:      printf("Server started. \nWaiting for client..\n");
  76:      listen(server_sock, 1);
  77:   
  78:      // 클라이언트의 연결요청 수락
  79:      clntlen = sizeof(client_addr);
  80:      if((client_sock = accept(server_sock, (struct sockaddr *)&client_addr, &clntlen)) < 0)
  81:      {
  82:          printf("Server: failed in acceptiong. \n");
  83:          exit(0);
  84:      }
  85:   
  86:      fork_ret = fork();
  87:      
  88:      if(0 == fork_ret)
  89:      {
  90:          // 자식 프로세스는 키보드 입력을 클라이언트로 송신
  91:          while(fgets(sendline, MAXLINE, stdin) != NULL)
  92:          {
  93:              strcpy(buffer, "수만 : ");
  94:              strcat(buffer, sendline);
  95:              size = strlen(buffer);
  96:              if(write(client_sock, buffer, strlen(buffer)) != size)
  97:              {
  98:                  printf("Error in write. \n");
  99:              }
 100:   
 101:              if(strstr(sendline, escapechar) != NULL)    //종료문자열 입력시 처리
 102:              {
 103:                  printf("Good bye. \n");
 104:                  close(client_sock);
 105:                  break;                
 106:              }
 107:          }
 108:      }
 109:      else if(0 < fork_ret)
 110:      {
 111:          // 부모 프로세스는 클라이언트로부터 수신된 메시지를 화면에 출력
 112:          while(1)
 113:          {
 114:              if((size = read(client_sock, recvline, MAXLINE)) < 0)
 115:              {
 116:                  printf("Error if read. \n");
 117:                  close(client_sock);
 118:                  exit(0);
 119:              }
 120:   
 121:              recvline[size] = '\0';
 122:              printf("%s", recvline);        //화면 출력
 123:          }
 124:      }
 125:   
 126:      close(server_sock);
 127:      close(client_sock);
 128:   
 129:      return 0;
 130:  }
 131:   
 132:  //시그널 처리함수
 133:  void z_handler()        
 134:  {
 135:      int state;
 136:   
 137:      waitpid(-1, &state, WNOHANG);
 138:      exit(0);        //자식이 소멸되며 부모도 같이 종료.
 139:   
 140:      return ;
 141:  }


● talk_client2.c

   1:  // 서버와 클라이언트 각자 따로 종료되도록 수정한 코드 talk_client.c
   2:  #include <stdio.h>
   3:  #include <string.h>
   4:  #include <stdlib.h>
   5:  #include <sys/types.h>
   6:  #include <signal.h>
   7:  #include <sys/socket.h>
   8:  #include <netinet/in.h>
   9:  #include <arpa/inet.h>
  10:  #include <sys/wait.h>
  11:   
  12:  #define    MAXLINE    1024
  13:   
  14:  void z_handler();        //시그널 처리함수
  15:   
  16:  char *escapechar = "exit"; //종료문자열
  17:   
  18:  int main(int argc, char *argv[])
  19:  {
  20:      char line[MAXLINE];
  21:      char sendline[MAXLINE];
  22:      char recvline[MAXLINE + 1];
  23:      char buffer[MAXLINE];
  24:      int n;
  25:      int size;
  26:      int comp;
  27:      int addr_size;
  28:      pid_t fork_ret;
  29:      static int s;
  30:      static struct sockaddr_in server_addr;
  31:      // signal사용하기 위해 추가된 변수들
  32:      int state;
  33:      struct sigaction act;
  34:      
  35:      if(argc != 3)
  36:      {
  37:          printf("Usage : ./%s serverIP serverPORT \n", argv[0]);
  38:          exit(0);
  39:      }
  40:   
  41:      // signal설정
  42:      act.sa_handler = z_handler;
  43:      sigemptyset(&act.sa_mask);
  44:      act.sa_flags = 0;
  45:   
  46:      state = sigaction(SIGCHLD, &act, 0);    //자식프로세스 종료시 발생하는 시그널 SIGCHLD이용
  47:   
  48:      if(0 != state)
  49:      {
  50:          fprintf(stderr, "sigaction() error. \n");
  51:          exit(1);
  52:      }
  53:      
  54:      // 소켓 생성
  55:      if((s = socket(PF_INET, SOCK_STREAM, 0)) < 0)
  56:      {
  57:          printf("Client: can't open stream socket. \n");
  58:          exit(0);
  59:      }
  60:   
  61:      // 소켓주소 구조체에 주소 세팅
  62:      bzero((char *)&server_addr, sizeof(server_addr));    //소켓주소 구조체 초기화
  63:      server_addr.sin_family = AF_INET;
  64:      server_addr.sin_addr.s_addr = inet_addr(argv[1]);
  65:      server_addr.sin_port = htons(atoi(argv[2]));
  66:   
  67:      // 서버에 연결 요청
  68:      if(connect(s, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
  69:      {
  70:          printf("Client: can't connect to server. \n");
  71:          exit(0);
  72:      }
  73:      
  74:      fork_ret = fork();
  75:      
  76:      if(0 == fork_ret)
  77:      {
  78:          // 자식 프로세스는 키보드 입력을 클라이언트로 송신
  79:          while(fgets(sendline, MAXLINE, stdin) != NULL)
  80:          {
  81:              strcpy(buffer, "수만: ");
  82:              strcat(buffer, sendline);
  83:              size = strlen(buffer);
  84:              if(write(s, buffer, strlen(buffer)) != size)
  85:              {
  86:                  printf("Error in write. \n");
  87:              }
  88:   
  89:              if(strstr(sendline, escapechar) != NULL)    //종료문자열 입력시 처리
  90:              {
  91:                  printf("Good bye. \n");
  92:                  close(s);
  93:                  exit(0);
  94:              }
  95:          }
  96:      }
  97:      else if(0 < fork_ret)
  98:      {
  99:          // 부모 프로세스는 클라이언트로부터 수신된 메시지를 화면에 출력
 100:          while(1)
 101:          {
 102:              if((size = read(s, recvline, MAXLINE)) < 0)
 103:              {
 104:                  printf("Error if read. \n");
 105:                  close(s);
 106:                  exit(0);
 107:              }
 108:   
 109:              recvline[size] = '\0';
 110:              printf("%s", recvline);        //화면 출력
 111:          }
 112:      }
 113:   
 114:      close(s);
 115:   
 116:      return 0;
 117:  }
 118:   
 119:  //시그널 처리함수
 120:  void z_handler()        
 121:  {
 122:      int state;
 123:   
 124:      waitpid(-1, &state, WNOHANG);
 125:      exit(0);
 126:   
 127:      return ;
 128:  }



<실행결과>

image 

서버측에서 “exit”를 입력하면 서버측 자식프로세스가 먼저 종료되고 시그널이 발생한다.
              시그널이 발생하면 시그널처리함수가 실행되어 자식프로세스의 리턴값을 받고 부모프로세스도 종료된다.
이 때 ps명령으로 메모리에 상주된 프로세스의 목록을 확인하니 클리이언트의 부모와 자식프로세스가 동작 중이다.
클라이언트측도 마찬가지로 자식프로세스가 소멸되면 부모프로세스도 같이 종료된다.
다시 ps명령으로 확인하니 모든 프로세스가 정상적으로 종료되었다.
잇힝~

image

자식프로세스에서 fgets( )로 이스케이프 문자열 “exit”를 입력받으면 종료되기만 하고,
바로 시그널이 발생하여 시그널처리함수가 호출된다.
시그널처리함수는 waitpid( )로 자식프로세스의 리턴값처리를 하여 좀비프로세스가 되는 것을 막고 바로 부모프로세서 자신도 종료한다.
클라이언트든 서버든 “exit”를 입력하여 종료하면 자연스럽게 소켓연결은 끊기게되어 서버와 클라이언트간 파이프는 깨진다.
그래서 상대방이 동작 중이더라도 메시지를 주고 받을 순 없다.
잇힝~