티스토리 뷰

개발/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_ralias되어 있기 때문에 크게 위 함수가 결국 strtok_r을 살펴보는 것과 같다. 이 내용에 대해서는 더 깊이 살펴볼 필요가 없다. 글의 취지와 맞지 않으니까.


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


우리는 strtokstrtok_r의 원형을 모두 살펴보았기 때문에 strtok_r의 마지막 인자인 char **save_ptr에 대해서도 잘 이해할 수 있다. strtok_rman 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
댓글
댓글쓰기 폼