what is _IO_acquire_lock in FSOP

C
_IO_fwrite (const void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
  _IO_size_t request = size * count;
  _IO_size_t written = 0;
  CHECK_FILE (fp, 0);
  if (request == 0)
    return 0;
  _IO_acquire_lock (fp);
  if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
    written = _IO_sputn (fp, (const char *) buf, request);
  _IO_release_lock (fp);
  /* We have written all of the input in case the return value indicates
     this or EOF is returned.  The latter is a special case where we
     simply did not manage to flush the buffer.  But the data is in the
     buffer and therefore written as far as fwrite is concerned.  */
  if (written == request || written == EOF)
    return count;
  else
    return written / size;
}

우리가 fsop를 하다보면 _IO_acquire_lock 에서 sigfault가 나는 경우가 정말 많이 있다.

왜 발생하는 거고 이 함수가 뭘까?

_IO_acquire_lock(fp)는 GNU C Library (glibc) 내부에서 사용하는 입출력 관련 스트림 구조체에 대한 락(lock)을 획득하는 함수이다. 이 함수는 주로 stdio (예: printf, fgets, fread, fwrite 등) 함수들이 멀티스레드 환경에서 안전하게 파일 스트림에 접근하도록 보장하기 위해 사용이 된다.

즉, 우리가 흔히 아는 세마폴과 뮤텍스를 실행하는 함수이다.

근데 이 lock을 거는 것은 실제로 glibc가 빌드되는 환경에 따라 분명이 다를 것이다.

C
libio/libioP.h, line 811 (as a macro)
sysdeps/generic/stdio-lock.h, line 51 (as a macro)
sysdeps/nptl/stdio-lock.h, line 91 (as a macro)

다음과 같이 _IO_acquire_lock 은 매크로가 다음과 같이 3군대에서 정의가 되어 있는데 싱글 스레드 → libio/libioP.h

2.2x → sysdeps/generic/stdio-lock.h

2.3+ → sysdeps/nptl/stdio-lock.h

으로 생각하면 된다. #include "libioP.h" 만 있다고 속으면 안된다. include는 libio/libioP.h 안에서도 추가적으로 일어날 수 있기 때문이다.

현재는 2.27 환경이 므로 sysdeps/generic/stdio-lock.h 코드를 보는데 사실 sysdeps/nptl/stdio-lock.h 와 차이가 없다. 코드의 위치가 조금 달라졌을 뿐이다.

C
# define _IO_acquire_lock(_fp) \
  do {									      \
    _IO_FILE *_IO_acquire_lock_file					      \
	__attribute__((cleanup (_IO_acquire_lock_fct)))			      \
	= (_fp);							      \
    _IO_flockfile (_IO_acquire_lock_file);
#  define _IO_acquire_lock_clear_flags2(_fp) \
  do {									      \
    _IO_FILE *_IO_acquire_lock_file					      \
	__attribute__((cleanup (_IO_acquire_lock_clear_flags2_fct)))	      \
	= (_fp);							      \
    _IO_flockfile (_IO_acquire_lock_file);
# else
#  define _IO_acquire_lock(_fp) _IO_acquire_lock_needs_exceptions_enabled
#  define _IO_acquire_lock_clear_flags2(_fp) _IO_acquire_lock (_fp)
# endif
# define _IO_release_lock(_fp) ; } while (0)

상당히 난잡한 코드인데 진짜 아름다운 코드인다.

여기서 보면

C
# define _IO_acquire_lock(_fp) \
  do {									      \
    _IO_FILE *_IO_acquire_lock_file					      \
	__attribute__((cleanup (_IO_acquire_lock_fct)))			      \
	= (_fp);							      \
    _IO_flockfile (_IO_acquire_lock_file);

이렇게 잘려있는 것을 알 수 있다. 잘려있는 것이 설계이다. 무슨 말이냐면

C
  _IO_acquire_lock (fp);
  if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
    written = _IO_sputn (fp, (const char *) buf, request);
  _IO_release_lock (fp);

이게 기존 코드이고 매크로를 적용해보면

C
do {									      \
    _IO_FILE *_IO_acquire_lock_file					      \
	__attribute__((cleanup (_IO_acquire_lock_fct)))			      \
	= (_fp);							      \
    _IO_flockfile (_IO_acquire_lock_file);
    
    
    if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
	    written = _IO_sputn (fp, (const char *) buf, request);
	    
_IO_release_lock (fp);

이렇게 된다. 그리고 _IO_release_lock 는

# define _IO_release_lock(_fp) ; } while (0) 이므로 얘도 매크로를 원래 코드에 적용해보면

C
do {									      \
    _IO_FILE *_IO_acquire_lock_file					      \
	__attribute__((cleanup (_IO_acquire_lock_fct)))			      \
	= (_fp);							      \
    _IO_flockfile (_IO_acquire_lock_file);
    
    
    if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
	    written = _IO_sputn (fp, (const char *) buf, request);
	    
} while (0)

가 된다. 그럼 이제 하나의 do-while 블록이 생기고

C
_IO_FILE *_IO_acquire_lock_file	__attribute__((cleanup (_IO_acquire_lock_fct)))	= (_fp);							    
_IO_flockfile (_IO_acquire_lock_file);

다음과 같은 코드가 있는 것과 같다.

첫번째 줄을 보면 _IO_FILE* 형식인 _IO_acquire_lock_file 변수에 = (_fp); _fp 를 넣는다 라는 말이다. 근데 __attribute__ 를 이 변수에 붙여서 변수가 뭔 변화가 일어났을때 어떤 함수를 실행할 것이다 를 지정한다. 이때 cleanup은 변수의 생명이 끝났을때를 의미하므로

즉, _IO_acquire_lock_file 변수의 생명이 끝났을때 _IO_acquire_lock_fct 함수를 실행하겠다 라는 의미이다.

FILE *_IO_acquire_lock_file … = (_fp);지역변수 선언① 매크로가 받은 스트림 포인터를 저장. ② cleanup(attr) 덕분에 블록을 빠져나올 때 자동으로 해제 함수가 호출됩니다.
__attribute__((cleanup(_IO_acquire_lock_fct)))GCC cleanup 속성변수의 수명이 끝나는 순간 _IO_acquire_lock_fct(&var) 가 자동 실행 → 내부에서 funlockfile() 호출.

그런데 변수의 생명은 일반적으로 한 함수내 즉, 한 블록이다. 그래서 do-while 문으로 감싸놓아서 do-while 문을 나갈때 변수 생명이 끝나게 해 _IO_acquire_lock_fct 함수를 실행하게 하는 아름다운 코드이다. 실제적인 lock은 아래 코드인

_IO_flockfile (_IO_acquire_lock_file);

에서 진행된다.

C
#  define _IO_flockfile(_fp) \
  if (((_fp)->_flags & _IO_USER_LOCK) == 0) _IO_lock_lock (*(_fp)->_lock)

다음과 같은데

여기서 flag 검사를 한번하고 _IO_lock_lock (*(_fp)->_lock) _lock 필드의 값을 가지고 정말 lock을 걸려고 한다.

깊숙한 정보
C
#define __libc_lock_lock_recursive(NAME)				      \
  ({ __libc_lock_recursive_t *const __lock = &(NAME);			      \
     void *__self = __libc_lock_owner_self ();				      \
     if (__mutex_trylock (&__lock->mutex)				      \
	 || (__lock->owner != __self					      \
	     && (__mutex_lock (&__lock->mutex), 1)))			      \
       __lock->owner = __self, __lock->count = 1;			      \
     else								      \
       ++__lock->count;							      \
  })

뭐 다음과 같이 mutex를 사용하게 된다 결국은

이때 *(_fp)->_lock 에서 주로 segfault가 발생하게 된다. 왜냐면 우리가 0x0같은 값으로 덮기 때문이다. 그래도 *(_fp)->_lock 을 뭐 굳이 설정할 필요 없이 (((_fp)->_flags & _IO_USER_LOCK) 을 해결하면 된다.

C
#define _IO_USER_LOCK 0x8000

그럼 이제 다시 돌아가서 변수의 생명이 끝났을때 실행되는

_IO_acquire_lock_fct 함수에 대해 알아보자

여기도 은근히 간단한데

C
_IO_acquire_lock_fct (_IO_FILE **p)
{
  _IO_FILE *fp = *p;
  if ((fp->_flags & _IO_USER_LOCK) == 0)
    _IO_funlockfile (fp);
}

다음과 같다 _IO_USER_LOCK 으로 user가 락을 건게 아니라면 이제 _IO_funlockfile 함수로 unlock을 시도한다.

C
#  define _IO_funlockfile(_fp) \
  if (((_fp)->_flags & _IO_USER_LOCK) == 0) _IO_lock_unlock (*(_fp)->_lock)

같은 원리다.