Published using Google Docs
무서운 SIGPIPE 소켓 프로그래밍의 함정
Updated automatically every 5 minutes

출처: http://doi-t.hatenablog.com/entry/2014/06/10/033309 

연결이 끊어진 소켓에 쓰기( send or write 등)를 하면 SIGPIPE 시그널이 발생하여 프로세스가 종료하기 때문에 제대로 SIGPIPE 시그널을 핸들링 해 두자는 이야기이다.

다음은 샘플 코드를 사용하여 실제로 파이프의 기입 처를 kill 하여 SIGPIPE 발생을 유사 체험 해 본다.

SISGPIPE를 받은 프로세스의 행동과 소켓 프로그래밍에서의 대응책

"sigpipe"로 검색하면 같은 이야기는 얼마든지 기사가 있고, 예를 들면,

"내가 쓴 서버가 갑자기 사망하는 거예요 이유는 무엇입니까?' 라는 질문을 받을 수 있다. 이것은 종종 SIGPIPE 처리를 잊는 것이 원인이다. SIGPIPE는 절단된 네트워크 소켓 등에 데이터를 쓰려고 했을 때 전달되는 UNIX 시그널이다. 특히 설정하지 않으면, 프로세스는 SIGPIPE를 받으면 종료한다. 따라서 통신이 갑자기 끊어 질 수있는 TCP 서버에서는 SIGPIPE를 무시하도록 설정해야 한다.

제 6회 UNIX 프로그래밍의 급소 (3)

라고 설명되어 있기도 하는 등 비교적 자주 있는 이야기 같다.

SIGPIPE 시그널을 받은 프로세스의 기본 동작은 core 파일 조차 뱉지 않고 프로세스 종료 한다.

이 동작은 위 인용대로 소켓 프로그래밍을 하고있는 측면에서는 아무런 오류 출력도 없이 갑자기 다운하는 것이다.

대응으로서는(이것도 위 인용대로), 예를 들어 이전의 Signal 함수를 사용한다면 Signal(SIGPIPE, SIG_IGN); 라는 것을 프로그램 시작 직후에 넣어두는 것만으로 좋다.

이렇게 하면 send 실행 시 연결이 끊어져 있어도 프로세스를 종료하지 않고 단순히 send(2)가 -1을 돌려 준다.

프로세스의 종료 상태와 SIGPIPE의 발생을 확인

여기에서는 다음의 샘플에서 실제로 파이프의 기입처를 kill 하여 SIGPIPE의 발생을 유사 체험 해 본다.

소켓 프로그래밍적으로는 아래의

라고 생각해 주면 좋다.

즉 수신이 중단된 곳에 송신자가 wirte(2)를 실행 했을 때 어떻게 되는지를 본다.

strace 명령으로 SIGPIPE의 발생을 확인

SIGPIPE의 발생 확인은 strace 명령의 결과와 프로세스의 종료 상태 에서 하려고 생각한다.

다음은 위 예제 중 sigpipe.bash 을 실행하여 그 때 출력되는 trace.log의 끝(이 장의 논의에 필요한 부분 만)를 출력 한 결과이다.

먼저 strace 명령의 결과를 보면 write(2)가 실패하고 있다( EPIPE (Broken pipe))을 확인할 수 있다. 프로세스는 killed by SIGPIPE.

종료 상태의 결과를 출력하고 ${PIPESTATUS[@]} 는 bash 고유의 기능이다 (편리하다!).

exit status:141 143 는 sender.sh이 SIGPIPE에 의해 종료 (141) 한 것을 receiver.sh이 SIGTERM 종료(143) 한 것을 의미하고 있다.

적어도 Linux 는 128에 시그널 번호를 더한 것을 종료 상태로 하고 있는 것 같다.

Posix 에서는 반환 값은 128 이상이어야 한다고 정해져 있을뿐이지만 Linux 는 128+ 시그널 번호를 반환한다.

Linux의 표준 신호의 값을 확인하면 SIGPIPE : 13, SIGTERM : 15 이다.

reciever.sh는 kill 명령에서 SIGTERM 전송하고 종료하고있다. 즉 128 + 15 = 143

sender.sh는 대상 프로세스가 죽기 때문에 SIGPIPE 시그널이 발생하고 종료한다. 즉 128 + 13 = 141

종료 상태의 의미적으로도 확실히 SIGPIPE로 다운하고 있는지 확인할 수 있다.

(역자 추가)

 C++ 오픈 소스 라이브러리 Poco 라이브러리를 보면 위의 문제는 Unix 계열에서만 발생하는 문제이고 Linux에서는 발생하지 않는다고 한다.

관련 코드의 스크린 샷

CentOS 테스트 - SIGPIPE

정말 linux 에서는 연결이 끊어진 소켓에 쓰기를 하여도 시그널이 발생하지 않는지 테스트 하였다.

결과는 리눅스에서는 시그널이 발생하지 않았다.

즉 SIGPIPE 시그널 관리는 유닉스 계열(OS X 포함)에서만 하면 된다.

예제 코드

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>

#define DEFAULT_PORT    32451
#define MAX_CONN        16
#define MAX_EVENTS      32
#define BUF_SIZE        16
#define MAX_LINE        256

void server_run();

int main()
{
   server_run();
   
return 0;
}

static void epoll_ctl_add(int epfd, int fd, uint32_t events)
{
   
struct epoll_event ev;
   ev.events = events;
   ev.data.fd = fd;
   
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == -1) {
       perror(
"epoll_ctl()\n");
       
exit(1);
   }
}

static void set_sockaddr(struct sockaddr_in *addr)
{
   bzero((
char *)addr, sizeof(struct sockaddr_in));
   addr->sin_family = AF_INET;
   addr->sin_addr.s_addr = INADDR_ANY;
   addr->sin_port = htons(DEFAULT_PORT);
}

static int setnonblocking(int sockfd)
{
   
if (fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0) | O_NONBLOCK) ==
       -1) {
       
return -1;
   }
   
return 0;
}

/*
* epoll echo server
*/

void server_run()
{
   
int i;
   
int n;
   
int epfd;
   
int nfds;
   
int listen_sock;
   
int conn_sock;
   
int socklen;
   
char buf[BUF_SIZE];
   
struct sockaddr_in srv_addr;
   
struct sockaddr_in cli_addr;
   
struct epoll_event events[MAX_EVENTS];

   listen_sock = socket(AF_INET, SOCK_STREAM, 0);

   set_sockaddr(&srv_addr);
   bind(listen_sock, (struct sockaddr *)&srv_addr,
sizeof(srv_addr));

   setnonblocking(listen_sock);
   listen(listen_sock, MAX_CONN);
   
printf("listening port: %d:\n", DEFAULT_PORT);

   epfd = epoll_create(1);
   epoll_ctl_add(epfd, listen_sock, EPOLLIN | EPOLLOUT | EPOLLET);

   socklen =
sizeof(cli_addr);
   
for (;;) {
       nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
       
for (i = 0; i < nfds; i++) {
           
if (events[i].data.fd == listen_sock) {
               
/* handle new connection */
               conn_sock = accept(listen_sock,
                              (struct sockaddr *)&cli_addr,
                              (
socklen_t*)&socklen);

               inet_ntop(AF_INET, (
char *)&(cli_addr.sin_addr),
                         buf,
sizeof(cli_addr));
               
printf("[+] connected with %s:%d\n", buf,
                      ntohs(cli_addr.sin_port));

               setnonblocking(conn_sock);
               epoll_ctl_add(epfd, conn_sock,
                             EPOLLIN | EPOLLET | EPOLLRDHUP |
                             EPOLLHUP);
           }
else if (events[i].events & EPOLLIN) {
               
/* handle EPOLLIN event */
               
for (;;) {
                   bzero(buf,
sizeof(buf));
                   n = read(events[i].data.fd, buf,
                           
sizeof(buf));
                   
if (n <= 0 /* || errno == EAGAIN */ ) {
                       
break;
                   }
else {
                       
printf("[+] data: %s\n", buf);
                       write(events[i].data.fd, buf,
                             
strlen(buf));
                   }
               }
           }
else {
               
printf("[+] unexpected\n");
           }
           
/* check if the connection is closing */
           
if (events[i].events & (EPOLLRDHUP | EPOLLHUP)) {
               
printf("[+] connection closed\n");
               epoll_ctl(epfd, EPOLL_CTL_DEL,
                         events[i].data.fd,
NULL);
               close(events[i].data.fd);

               
// SIGPIPE 테스트를 위해 접속이 끊어진 소켓에 일부러 쓰기를 한다
               
printf("SIGPIPE test !!!\n");
               
char sendData[] ="sigpipe";
               
if(write(events[i].data.fd, sendData, strlen(sendData)) < 0) {
                   perror(
"send closed socket\n");
               }
               
continue;
           }
       }
   }
}

아래 실행 결과를 보면 접속이 끊어진 상태에서 일부러 쓰기를 하면 에러만 발생하고, 이후 새로운 접속을 문제 없이 받아들인다.

즉 SIGPIPE 시그널을 핸들링 하지 않아도 프로그램이 죽지 않는다.