futex() 라는 것이 왜 system call() 로 되어 있는지 궁금합니다.
글쓴이: trymp / 작성시간: 금, 2016/02/12 - 10:54오전
linux 의 futex() 에 대해서 공부하고 있는데요.
도입목적이 효율성이라고 들었습니다.
아주 간단한 critical section 때문에 많은 cpu clock 을 소모하지 않고자
sleep 이나 스케쥴링이 안 일어나도록 user space 에서 작업하는 것이 목적이라고 들었습니다.
근데 실제로는 system call() 을 호출하던데요.
그러면 커널모드로 진입하게 되고 경우에 따라서는 스케쥴링도 될텐데
그럼 말짱 꽝 아닌가요???
제가 잘못 이해하고 있는지 모르겠지만 뭔가 모순된 부분이 있는 것 같아서
질문드립니다.
고수님들의 조언 부탁드립니다.
Forums:
?
종래의 Mutex 는 반드시 시스템 콜을 호출하여야 커널 내에서 Lock 을 걸든, Unlock 하든
할 수 있습니다. 즉 구현된 유저레벨 함수를 호출하면 내부 구현에선 별 다른 핵심 작업 없이
무조건 시스템 콜을 호출해야 한다는 얘기입니다.
그러나 Futex 는 이름 그대로 Fast Userspace muTEX 입니다.
이 말의 의미는 부분적으로 유저 레벨에서 Lock/Unlock 처리를 할 수 있는 인터페이스를 제공한다는 의미입니다.
간단한 예로 glibc 의 pthread_mutex_lock / pthread_mutex_unlock 등의 함수 구현을 봅시다.
(Intel TSX 의 Lock Elision 때문에 코드 보기가 상당히 더러워졌습니다.. x86 기준으로 보겠습니다.)
다른건 제쳐놓고, futex_wait 만 보면 어떤식으로 사용이 되는지 한 번 확인해 봅니다.
먼저 nptl/pthread_mutex_lock.c 를 보면 lll_lock 을 여러 곳에서 호출하고 있습니다.
이는 sysdeps/unix/sysv/linux/i386/lowlevellock.h 을 보면 인라인 어셈블리로 되어 있습니다.
여기서는 대략 아래처럼 되어 있습니다.
#define lll_lock(futex, private) \
(void) \
({ int ignore1, ignore2; \
if (__builtin_constant_p (private) && (private) == LLL_PRIVATE) \
__asm __volatile (__lll_lock_asm_start \
"jz 18f\n\t" \
"1:\tleal %2, %%ecx\n" \
"2:\tcall __lll_lock_wait_private\n" \
"18:" \
: "=a" (ignore1), "=c" (ignore2), "=m" (futex) \
: "0" (0), "1" (1), "m" (futex), \
"i" (MULTIPLE_THREADS_OFFSET) \
: "memory"); \
else \
{ \
int ignore3; \
__asm __volatile (__lll_lock_asm_start \
"jz 18f\n\t" \
"1:\tleal %2, %%edx\n" \
"0:\tmovl %8, %%ecx\n" \
"2:\tcall __lll_lock_wait\n" \
"18:" \
: "=a" (ignore1), "=c" (ignore2), \
"=m" (futex), "=&d" (ignore3) \
: "1" (1), "m" (futex), \
"i" (MULTIPLE_THREADS_OFFSET), "0" (0), \
"g" ((int) (private)) \
: "memory"); \
} \
})
__lll_lock_asm_start 는 아래와 같습니다.
# define __lll_lock_asm_start LOCK_INSTR "cmpxchgl %1, %2\n\t"
락에서 흔히 볼 수 있는 핵심 연산인 CAS 입니다. 이 연산은 아시다시피 atomic 입니다.
다음 call __lll_lock_wait (또는 __lll_lock_wait_private) 명령으로 또 다른 함수를 호출합니다.
이는 sysdeps/unix/sysv/linux/i386/i486/lowlevellock.S 에 정의되어 있습니다.
쓸데없는 코드때문에 복잡하니 핵심만 간추려서 보면 아래와 같습니다.
__lll_lock_wait:
pushl %edx
pushl %ebx
pushl %esi
movl %edx, %ebx
movl $2, %edx
xorl %esi, %esi /* No timeout. */
LOAD_FUTEX_WAIT (%ecx)
cmpl %edx, %eax /* NB: %edx == 2 */
jne 2f
1: movl $SYS_futex, %eax
ENTER_KERNEL
2: movl %edx, %eax
xchgl %eax, (%ebx) /* NB: lock is implied */
testl %eax, %eax
jnz 1b
popl %esi
popl %ebx
popl %edx
ret
사실 길지도 않고 매우 단순한 형태인데, 저기서 핵심은 1: 과 2: 부분입니다.
옆에 주석으로 설명되어 있듯이 일단 1: 은 딱 봐도 아시겠지만 futex syscall 호출 루틴입니다.
그런데 앞에서 eax 가 2가 아닌 경우는 2: 로 점프합니다. 이 경우는 뒤에서 xchg 연산을 해주고 있습니다.
즉, 이미 lock 이 걸린 리소스에 대해서 lock 을 시도하는 경우 syscall 을 호출하여 wait 합니다.
이 체크를 유저 레벨에서 하고 있습니다. 시스템 콜이 호출되면 말씀하신 대로 스케줄링이 일어납니다.(아예 큐에서 빠집니다.)
저 메모리 값의 의미나 자세한 건 futex 의 구현에 따른 definition 이니 따로 참조하시면 됩니다.
만약 락 연산 자체를 시스템 콜에서 수행한다면, 이러한 작업은 필요가 없습니다.
단순히 락을 구분할 수 있는 메모리 값 정도만 시스템 콜로 넘겨주면 될 것입니다.
그러나 futex 는 받은 인자(eax) 의 주소에 있는 값을 통해 유저 레벨에서 처리합니다.
이로 인해 시스템 콜을 호출할 일이 줄어듭니다. 락이 걸려있지 않은 상태에서 락을 처음으로
걸려고 하는 상황에서는 굳이 커널 진입을 할 필요가 없는 것이죠.
https://www.akkadia.org/drepp
https://www.akkadia.org/drepper/futex.pdf
두 분 감사합니다..
^^
댓글 달기