what is _IO_acquire_lock in FSOP
_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가 빌드되는 환경에 따라 분명이 다를 것이다.
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 와 차이가 없다. 코드의 위치가 조금 달라졌을 뿐이다.
# 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)상당히 난잡한 코드인데 진짜 아름다운 코드인다.
여기서 보면
# 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);이렇게 잘려있는 것을 알 수 있다. 잘려있는 것이 설계이다. 무슨 말이냐면
_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);이게 기존 코드이고 매크로를 적용해보면
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) 이므로 얘도 매크로를 원래 코드에 적용해보면
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 블록이 생기고
_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);
에서 진행된다.
# define _IO_flockfile(_fp) \
if (((_fp)->_flags & _IO_USER_LOCK) == 0) _IO_lock_lock (*(_fp)->_lock)다음과 같은데
여기서 flag 검사를 한번하고 _IO_lock_lock (*(_fp)->_lock) _lock 필드의 값을 가지고 정말 lock을 걸려고 한다.
깊숙한 정보
#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) 을 해결하면 된다.
#define _IO_USER_LOCK 0x8000그럼 이제 다시 돌아가서 변수의 생명이 끝났을때 실행되는
_IO_acquire_lock_fct 함수에 대해 알아보자
여기도 은근히 간단한데
_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을 시도한다.
# define _IO_funlockfile(_fp) \
if (((_fp)->_flags & _IO_USER_LOCK) == 0) _IO_lock_unlock (*(_fp)->_lock)같은 원리다.