Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

229224
1178
109351


[라즈베리파이] 비트 처리 __test_and_set_bit() __test_and_clear_bit() 함수 동작 원리 Linux Kernel - Core Analysis

리눅스 커널과 드라이버에서 __test_and_set_bit()와 __test_and_clear_bit() 함수를 많이 씁니다.


두 함수 중 test_and_set_bit()를 써서 비트를 처리하는 코드를 보겠습니다.
다음은 워크를 워크큐에 큐잉하는 queue_work_on() 함수입니다.
[kernel/workqueue.c]
1 bool queue_work_on(int cpu, struct workqueue_struct *wq,
2    struct work_struct *work)
3 {
4 bool ret = false;
5 unsigned long flags;
6
7 local_irq_save(flags);
8
9 if (!test_and_set_bit(WORK_STRUCT_PENDING_BIT, work_data_bits(work))) {
10 __queue_work(cpu, wq, work);
11 ret = true;
12 }
13
14 local_irq_restore(flags);
15 return ret;
16}

9번째 줄 코드를 보겠습니다. 
9 if (!test_and_set_bit(WORK_STRUCT_PENDING_BIT, work_data_bits(work))) {
10 __queue_work(cpu, wq, work);
11 ret = true;
12 }

work_data_bits(work) 매크로 함수는 struct work_struct 구조체 주소에서 struct work_struct.data 멤버를 읽습니다. 이 값이 WORK_STRUCT_PENDING_BIT(1)이면 9~12줄 코드를 실행하지 않고 if 문을 빠져나옵니다.

test_and_set_bit() 함수는 리눅스 커널 자료구조 함수 중 하나입니다.
test_and_set_bit(A, B); 와 같이 호출하면 A와 B란 변수 비트를 AND 연산한 다음 결과가 1이면 1을 리턴하고 반대로 0이면 0을 리턴합니다. 연산 결과에 상관없이 B이란 변수에 A비트를 설정합니다.

이해를 돕기 위해 9~12줄 코드를 다른 코드로 쉽게 표현하면 다음과 같습니다.
1 if (work->data == WORK_STRUCT_PENDING_BIT) {
3 } else
4 work->data =| WORK_STRUCT_PENDING_BIT;
5 __queue_work(cpu, wq, work);
6 ret = true;
7 }

work->data가 WORK_STRUCT_PENDING_BIT이면 if 문을 만족하는데 2번 줄 코드와 같이 동작도 안 하고 if 문을 빠져나옵니다. 2번째 줄 코드와 같이 실행할 코드가 없기 때문입니다. 대신 work->data가 WORK_STRUCT_PENDING_BIT 가 아니면 else문을 실행합니다. 

if 문 조건을 만족하면 아무 동작을 안 하는 코드는 모양이 이상하니 if 문에 ! 조건으로 바꿔  코드 순서를 바꿔 보면 다음과 같습니다.
if ( !(work->data == WORK_STRUCT_PENDING_BIT)) {
work->data =| WORK_STRUCT_PENDING_BIT;
__queue_work(cpu, wq, work);
ret = true;
}

work->data 멤버가 WORK_STRUCT_PENDING_BIT 매크로가 아니면 work->data에 WORK_STRUCT_PENDING_BIT를 저장하고 __queue_work() 함수를 호출하는 겁니다.

test_and_set_bit() 함수를 다른 코드로 바꿔서 설명을 드렸습니다. test_and_set_bit() 함수는 위에 바꾼 코드와 같이 struct work_struct.data 멤버가 WORK_STRUCT_PENDING_BIT 매크로가 아니면 struct work_struct.data 멤버를 WORK_STRUCT_PENDING_BIT 매크로로 설정하고 0을 리턴합니다.

반대로 struct work_struct.data 멤버가 WORK_STRUCT_PENDING_BIT 매크로이면 struct work_struct.data 멤버를 WORK_STRUCT_PENDING_BIT 매크로로 설정한 후 1을 리턴합니다.

여기까지 test_and_set_bit() 함수를 어느 커널 코드에서 쓰는지 알아봤으니 이번에 이 함수가 어떻게 동작히는지 확인합시다.

해당 코드 구현부는 다음 코드에서 확인할 수 있습니다.
https://elixir.bootlin.com/linux/v4.14.43/source/include/asm-generic/bitops/non-atomic.h#L58
static inline int __test_and_set_bit(int nr, volatile unsigned long *addr)
{
unsigned long mask = BIT_MASK(nr);
unsigned long *p = ((unsigned long *)addr) + BIT_WORD(nr);
unsigned long old = *p;

*p = old | mask;
return (old & mask) != 0;
}

static inline int __test_and_clear_bit(int nr, volatile unsigned long *addr)
{
unsigned long mask = BIT_MASK(nr);
unsigned long *p = ((unsigned long *)addr) + BIT_WORD(nr);
unsigned long old = *p;

*p = old & ~mask;
return (old & mask) != 0;
}

__test_and_set_bit() 함수와 __test_and_clear_bit() 함수  구현부를 그대로 가져와 리눅스 시스템 프로그래밍으로 동작을 확인합니다.  먼저 소스 코드를 소개합니다.
240 #include <stdio.h>
241 enum {
242         CHAINIV_STATE_INUSE = 1
243         CHAINIV_STATE_DISUSE = 2,
244         CHAINIV_STATE_ONLINE = 3,
245         CHAINIV_STATE_OFFLINE = 4
246 };
247
248
249 int __test_and_clear_bit(int nr, volatile unsigned long *addr)
250 {
251          unsigned long mask = (1UL << ((nr) % 32));
252          unsigned long *p = ((unsigned long *)addr) + ((nr) / 32);
253          unsigned long old = *p;
254
255          *p = old & ~mask;
256          return (old & mask) != 0;
257 }
258
259 int __test_and_set_bit(int nr, volatile unsigned long *addr)
260 {
261         unsigned long mask = (1UL << ((nr) % 32));
262         unsigned long *p = ((unsigned long *)addr) + ((nr) / 32);
263         unsigned long old = *p;
264
265         *p = old | mask;
266         return (old & mask) != 0;
267 }
268
269 int main()
270 {
271         unsigned long state = 0;
272
273         printf(" state: %ld, line[%d] \n", state, __LINE__);
274
275         if(__test_and_clear_bit(CHAINIV_STATE_INUSE, &state)) {
276                 printf(" state: %ld, line[%d] \n", state, __LINE__);
277         } else {
278                 printf(" state: %ld, line[%d] \n", state, __LINE__);
279         }
280
281         if(__test_and_set_bit(CHAINIV_STATE_INUSE, &state)) {
282                 printf(" state: %ld, line[%d] \n", state, __LINE__);
283         } else {
284                 printf(" state: %ld, line[%d] \n", state, __LINE__);
285         }
286
287         if(__test_and_set_bit(CHAINIV_STATE_INUSE, &state)) {
288                 printf(" state: %ld, line[%d] \n", state, __LINE__);
289         } else {
290                 printf(" state: %ld, line[%d] \n", state, __LINE__);
291         }
292
293         if(__test_and_clear_bit(CHAINIV_STATE_INUSE, &state)) {
294                 printf(" state: %ld, line[%d] \n", state, __LINE__);
295         } else {
296                 printf(" state: %ld, line[%d] \n", state, __LINE__);
297         }
298
299         return 1;
300 }

리눅스 시스템 프로그램으로 작성하면 되는데 위 코드를 작성 후
main.c 파일로 저장한 후 다음 명령어로 컴파일을 합시다.
gcc -o test_main main.c

컴파일 후 test_main 어플리케이션을 실행하면 다음과 같은 결과를 볼 수 있습니다.
state: 0, line[273]
state: 0, line[278]
state: 1, line[284]
state: 1, line[288]
state: 0, line[294]

위 로그가 어떻게 출력됐는지 해당 코드를 자세히 살펴봅시다.

첫 로그 실행 코드를 봅시다.
271         unsigned long state = 0;
272
273         printf(" state: %ld, line[%d] \n", state, __LINE__);

state이란 변수가 0이니 0을 출력합니다.

이번에는 두 번째 로그입니다.
275         if(__test_and_clear_bit(CHAINIV_STATE_INUSE, &state)) {
276                 printf(" state: %ld, line[%d] \n", state, __LINE__);
277         } else {
278                 printf(" state: %ld, line[%d] \n", state, __LINE__);
279         }

CHAINIV_STATE_INUSE 값이 1이므로 state가 0이니 __test_and_clear_bit() 함수는 0을 반환합니다.
동시에 state 값인 0 에서 1을 Clear합니다. 결과는 0입니다.

따라서 다음과 같이 278줄 코드를 실행하는 로그를 출력합니다.
state: 0, line[278]

다음 세 번째 로그 실행 코드를 분석합시다.
281         if(__test_and_set_bit(CHAINIV_STATE_INUSE, &state)) {
282                 printf(" state: %ld, line[%d] \n", state, __LINE__);
283         } else {
284                 printf(" state: %ld, line[%d] \n", state, __LINE__);
285         }

state 값이 0이니 __test_and_set_bit() 함수는 0을 반환합니다.
따라서 else문인 284 코드를 실행합니다. 동시에 state를 1로 설정합니다.

해당 로그는 다음과 같습니다.
state: 1, line[284]

이번에 마지막 로그를 분석합니다.
293         if(__test_and_clear_bit(CHAINIV_STATE_INUSE, &state)) {
294                 printf(" state: %ld, line[%d] \n", state, __LINE__);
295         } else {
296                 printf(" state: %ld, line[%d] \n", state, __LINE__);
297         }

state가 1이니 __test_and_clear_bit() 함수는 1을 반환하며 1을 Clear합니다.
따라서 294줄 코드를 실행합니다.

결과 로그는 다음과 같습니다.
state: 0, line[294]

리눅스 커널에서 보이는 코드를 보면 너무 어렵게 생각말고 이렇게 코드를 분리해서 테스트 해 봅시다. 
바로 어떻게 동작하는지 확인할 수 있습니다.




덧글

댓글 입력 영역