티스토리 뷰

개발/Linux

strtok(), strtok_r() 뽀개기

Jaeyeon Baek 2017.03.16 10:33

문자열을 특정 구분자로 잘라서 파싱할 때 유용하게 사용되는 함수가 바로 strtok 되시겠다. 하지만 많은 웹사이트 글처럼 이 함수는 thread 에서 안전하지 못하다. 즉, 의도치 않게 동작할 수 있다는 의미인데 어디에도 명쾌하게 이유를 설명한 글이 없어서 직접 작성해본다.


우선 strtok 의 함수 원형을 glibc 에서 찾아보면 아래와 같다. 

/* Parse S into tokens separated by characters in DELIM.
   If S is NULL, the last string strtok() was called with is
   used.  For example:
    char s[] = "-abc-=-def";
    x = strtok(s, "-");     // x = "abc"
    x = strtok(NULL, "-=");     // x = "def"
    x = strtok(NULL, "=");      // x = NULL
        // s = "abc\0=-def\0"
*/
char *
strtok (char *s, const char *delim)
{
  static char *olds;
  return __strtok_r (s, delim, &olds);
}


친절하게 예제까지 첨부되어 있다. 함수 자체는 매우 간결한데, 두 가지를 주의 깊게 봐야 하겠다. 바로 static char *olds __strtok_r 을 호출한다는 것인데 하나씩 살펴보면 *olds 는 static 으로 선언되어 프로그램의 data 영역에 저장된다. data 영역에는 전역변수( global )도 포함되는데 어디서나 접근 가능한 변수라는 의미다. 즉, 임시로 사용되는 stack 영역이 아니기 때문에 multi-thread 에서 사용되면 atomic 하게 동작하지 않게 된다. 


그래서 대안으로 사용되는 함수가 strtok_r 인데 이번에도 함수 내용을 살펴보면 아래와 같다.

/* Parse S into tokens separated by characters in DELIM.
   If S is NULL, the saved pointer in SAVE_PTR is used as
   the next starting point.  For example:
    char s[] = "-abc-=-def";
    char *sp;
    x = strtok_r(s, "-", &sp);  // x = "abc", sp = "=-def"
    x = strtok_r(NULL, "-=", &sp);  // x = "def", sp = NULL
    x = strtok_r(NULL, "=", &sp);   // x = NULL
        // s = "abc\0-def\0"
*/
char *
__strtok_r (char *s, const char *delim, char **save_ptr)
{   
  char *end;
  
  if (s == NULL)
    s = *save_ptr;

  if (*s == '\0')
    {
      *save_ptr = s;
      return NULL;
    }
    
  /* Scan leading delimiters.  */
  s += strspn (s, delim);
  if (*s == '\0')
    {
      *save_ptr = s;
      return NULL;
    }

  /* Find the end of the token.  */
  end = s + strcspn (s, delim);
  if (*end == '\0')
    {
      *save_ptr = end;
      return s;
    }

  /* Terminate the token and make *SAVE_PTR point past it.  */
  *end = '\0';
  *save_ptr = end + 1;
  return s;
}


strtok_r 을 살펴보기로 하고 대뜸 __strtok_r 을 가져왔다. 이는 내부적으로 strtok_r __strtok_r alias 되어 있기 때문에 크게 위 함수가 결국 strtok_r 을 살펴보는 것과 같다. 이 내용에 대해서는 더 깊이 살펴볼 필요가 없다. 글의 취지와 맞지 않으니까.


여하튼 여기까지만 자세히 살펴보면 strtok 는 결국 strtok_r wrapping 함수라는 것을 알 수 있다. 단순히strtok 안에서 사용되는 static 한 변수 olds 를 애초에 함수의 인자로 받아서 처리하는 것이다. 그렇기 때문에 thread 에서 safety 하게 사용할 수 있게 된다.


우리는 strtok strtok_r 의 원형을 모두 살펴보았기 때문에 strtok_r 의 마지막 인자인 char **save_ptr 에 대해서도 잘 이해할 수 있다. strtok_r man page 에 좋은 예제가 있는데, 혹시 loop 안에서 strtok_r 을 중첩으로 사용하려면 **save_ptr 을 별도로 잘 관리해줘야 한다. 당연한 이야기지만 변수 한개가 여러 loop 에서 사용될 수 없다는 의미다.


아래 man page 의 일부 내용을 첨부하니, saveptr 쪽만 살펴보면 되겠다.

char *saveptr1, *saveptr2;

for (j = 1, str1 = argv[1]; ; j++, str1 = NULL) {
  token = strtok_r(str1, argv[2], &saveptr1);
  if (token == NULL)
    break;
  printf("%d: %s\n", j, token);

  for (str2 = token; ; str2 = NULL) {
    subtoken = strtok_r(str2, argv[3], &saveptr2);
    if (subtoken == NULL)
      break;
    printf(" --> %s\n", subtoken);
  }
}


사실 thread 를 염두하고 프로그래밍을 한다는 것은 좋지만 애초에 그 원리를 알고 확장성을 고려해서 thread unsafety 함수는 피하는게 좋은 습관이겠다.

'개발 > Linux' 카테고리의 다른 글

VIM 주석을 달아보자 (Doxygen)  (0) 2017.04.11
VIM 낱말 자동완성  (0) 2017.03.28
strtok(), strtok_r() 뽀개기  (0) 2017.03.16
sshd PermitRootLogin 기본 값 변경  (0) 2017.02.13
하드링크와 심볼릭링크 개념잡기  (0) 2017.01.05
DocuWiki 시간 동기화  (0) 2016.12.19
댓글
댓글쓰기 폼