수안이의 컴퓨터 연구실

  • Mainpage
  • About Me
  • Tags
  • Metapage
  • Notice
  • Location
  • Keywords
  • Guestbook
  • Admin
  • Write an Article
  • Total | 1693767
  • Today | 118
  • Yesterday | 588

2 Articles, Search for 'nested interrupt'

  1. 2007/05/10 리눅스 커널의 이해(3): 리눅스 디바이스 작성시 동기화 문제
  2. 2007/05/10 리눅스 커널의 이해(2): 리눅스 커널의 동작
Unix & Linux/Kernel2007/05/10 10:18

리눅스 커널의 이해(3): 리눅스 디바이스 작성시 동기화 문제

저자: 서민우
출처: Embedded World

[ 관련 기사 ]
♠ 리눅스 커널의 이해(1) : 커널의 일반적인 역할과 동작
♠ 리눅스 커널의 이해(2): 리눅스 커널의 동작

일반적으로 리눅스 디바이스 드라이버를 작성할 땐 여러 가지 동기화 문제를 고려해야 한다. 리눅스 디바이스 드라이버를 작성할 때 동기화 문제를 제대로 해결하지 않는다면 커널이 멈추는 등의 심각한 문제가 발생한다.

리눅스 디바이스 드라이버 내에서 동기화 문제가 발생하는 이유는 두 가지이다. 먼저 우리가 작성하는 디바이스 드라이버는 리눅스 커널의 주요한 여러 흐름(시스템 콜 영역, top half 영역, bottom half 영역) 속에서 동작한다. 다음은 nested interrupt나 process scheduling에 의해 리눅스 커널 내에서는 커널 영역간에 여러 가지 경쟁 상태가 발생할 수 있다.

따라서 우리는 리눅스 디바이스 드라이버를 작성할 때 발생할 수 있는 여러 가지 동기화 문제와 이에 대한 일반적인 해결책을 알아야 한다.

이번 기사에서는 이러한 동기화 문제와 이에 대한 해결책을 구체적으로 알아보기 전에 1) 동기화 문제란 무엇인지, 2) 디바이스 드라이버의 주요한 동작과 리눅스 커널의 흐름에서의 디바이스 드라이버의 위치, 3) nested interrupt와 process scheduling에 의한 리눅스 커널의 흐름을 구체적으로 알아보기로 한다.


동기화 문제

먼저 동기화 문제가 무엇인지 보기로 하자.

신호등이 있는 횡단보도를 생각해 보자. 보행자는 신호등에 빨간불이 들어와 있는 동안에는 횡단보도 한쪽 끝에 서 있다가 신호등에 녹색 불이 들어오면 횡단보도를 건넌다. 보행자가 횡단보도를 건너는 동안에 횡단보도를 지나려고 하는 차량은 일시 정지해 있어야 한다. 만약 보행자가 신호등의 녹색 불을 보고 횡단보도를 건너는 동안에 차량이 일시 정지해 있지 않고 횡단보도를 지나려고 할 경우 교통사고 등의 문제가 발생한다. 이러한 문제는 어느 순간에 횡단보도를 보행자와 차량이 동시에 이용하려고 하는 데서 발생한다. 즉, 보행자와 차량이 신호등에 맞추어 횡단보도를 순서대로 이용한다면 이러한 문제는 발생하지 않는다.

이처럼 동기화의 문제란 어떤 일의 순서를 지키지 않는 데서 발생하는 문제이다. 따라서 동기화란 어떤 일의 순서를 맞추는 일이다. 일반적으로 동기화의 문제는 공유영역(예를 들어, 횡단보도)을 중심으로 발생한다. 이러한 공유영역은 flag(예를 들어, 신호등)에 맞추어 순서대로 이용하여야 한다.

공유영역과 관련한 동기화의 문제는 쓰레드를 이용한 응용 프로그램, multi-tasking을 수행하는 커널 내부, 신호등을 제어하는 논리회로 등 여러 군데서 발생할 수 있다.

다음 예제를 통해 공유영역과 관련한 동기화의 문제가 어떻게 발생하는지 구체적으로 들여다 보자.

사용자 삽입 이미지



이 예제는 리눅스 쓰레드 프로그램이다. ①에서 pthread_create() 함수를 이용해 10개의 쓰레드를 생성하며, 각각의 쓰레드는 adder() 함수를 수행한다. adder() 함수에서 각각의 쓰레드는 global_counting 변수 값이 0x10000000보다 크거나 같을 때까지 변수 값을 증가시킨다. 여기서 global_counting 변수는 쓰레드 간에 공유하는 공유 변수이다. 즉, 공유 영역이다. adder() 함수 내에 있는 local_counting 변수는 각각의 쓰레드가 global_counting 변수 값을 얼마나 증가시켰는지를 보기 위한 변수이다. local_counting 변수 값은 adder() 함수에서 리턴 값으로 사용한다. 이 리턴 값을 main() 함수의 ②에서 pthread_join() 함수를 통해 전달 받은 후 main() 함수 내에 있는 sum_local_counting 변수에 더해준다. 여기서 pthread_join() 함수는 쓰레드가 종료되기를 기다리는 함수이다. main() 함수의 마지막 부분에서는 global_counting 변수 값과 sum_local_counting 변수 값을 출력해 준다.

참고로 pthread_create() 함수의 첫번째 인자는 변수의 주소 값이 넘어가지만, pthread_join() 함수의 첫번째 인자는 변수의 값이 넘어간다.

이 예제를 다음과 같이 컴파일 한다. 참고로 리눅스 상에서 쓰레드 프로그램을 컴파일 할 때는 posix thread 라이브러리를 써야 하며 따라서 컴파일 옵션에 –lpthread 가 들어가야 한다. 컴파일이 끝났으면 실행시켜 본다.


$ gcc race-condition.c -o race-condition -lpthread
$ ./race-condition
…
global counting: 0x10000000
sum of local counting: 0x5d9c9858
$ ./race-condition
…
global counting: 0x10000000
sum of local counting: 0x662979dc


두 번의 실행 결과 global_counting 변수 값은 각각 0x10000000이 나왔으나, sum_local_counting 변수 값은 각각 0x5d9c9858, 0x662979dc이 나왔다. 이 값은 몇 차례 반복해서 수행해도 같은 값이 거의 나오지 않는다. 이 두 변수의 값이 왜 다른지 [그림 1]을 보며 생각해 보자.

사용자 삽입 이미지

[그림 1] 공유영역에서의 쓰레드간 race condition


[그림 1]에서 timer interrupt에 의해 수행하는 부분은 hardware interrupt에 의해 시작하는 리눅스 커널의 일반적인 동작으로 <리눅스 커널의 이해 ②> 기사의 [그림 9]을 참조하기 바란다.

먼저 [그림 1]에서 다음과 같이 가정하자.

i) 굵은 선 부분은 adder() 함수의 ③ 부분을 나타낸다.
ii) T1과 T2는 ①에서 생성한 쓰레드 중 임의의 두 쓰레드이다.
iii) 쓰레드 T1의 A 지점은 adder() 함수의 A 지점이다.
iv) 쓰레드 T1이 A 지점을 수행할 때 tmp_counting 값은 0x10000이다.
v) 쓰레드 T1은 A 지점에서 할당 받은 time slice를 다 썼다.
vi) C 지점에서 스케쥴링시 쓰레드 T2가 선택된다.
vii) 쓰레드 T2는 E 지점에서 할당 받은 time slice를 다 썼다.
viii) 쓰레드 T2의 E 지점에서 F 지점까지 여러 번의 timer interrupt가 들어왔다.
ix) 쓰레드 T2는 F 지점에서 새로이 할당 받은 time slice를 다 썼다.
x) H 지점에서 스케쥴링시 쓰레드 T1이 다시 선택된다.

위 가정에서 viii)의 경우 쓰레드 T2의 E 지점에서 F 지점까지 timer interrupt가 여러 번 들어 오더라도 할당 받은 time slice가 남아 있으므로 중간에 스케쥴링을 수행하지 않으며, 따라서 또 다른 쓰레드를 수행하지는 않는다.

쓰레드 T1이 A 지점을 지나는 순간 global_counting 값은 가정 iv)에 의해 0x10000이다. A 지점에서 timer interrupt가 발생할 경우 가정 v)에 의해 B 부분에서 스케쥴링을 요청하고 C 부분에서 스케쥴링을 수행한다. 스케쥴링 결과 가정 vi)에 의해 쓰레드 T2가 선택되며, 따라서 C 부분에서 시작한 스케쥴링은 D 부분에서 끝난다. 즉, c 지점으로 들어가서 d 지점으로 나온다. 그러면 쓰레드 T2는 D 부분을 거쳐 E 지점으로 나와 첫 번째 을 수행한다. 이 때 쓰레드 T2의 tmp_counting 값도 0x10000이 된다. 이 후에 F 지점에 도착할 때까지 여러 번 을 수행한다. 편의상 여기서는 0x10000 번 수행한다고 가정한다. 그러면 F 지점 바로 전에 마지막으로 수행한 에서 global_counting 값은 0x20000이 된다. F 지점에서 timer interrupt가 발생할 경우 가정 ix)에 의해 G 부분에서 스케쥴링을 요청하고 H 부분에서 스케쥴링을 수행한다. 스케쥴링 결과 가정 x)에 의해 쓰레드 T1이 다시 선택된다. 따라서 H 부분에서 시작한 스케쥴링은 I 부분에서 끝난다. 그러면 쓰레드 T1은 I 부분을 거쳐 J 부분으로 나와 A 지점에서 잘린 의 나머지 부분을 수행한다. 그 결과 global_counting 값은 0x10001이 되며, 따라서 쓰레드 T2가 수행한 0x10000 번의 동작은 잃어버리게 된다.

각각의 쓰레드가 을 순서대로 접근을 했다면 이런 결과는 없었을 것이다. 즉, global_counting 값을 읽고 0x10000000보다 작을 경우 하나를 증가시키고 global_counting 값을 갱신하는 부분이 쓰레드 간에 겹치지 않았다면 중간값을 잃어버리는 일은 없었을 것이다.

일반적으로 각각의 흐름을 갖는 하나 이상의 루틴이 공유영역을 접근했을 때 동기화 문제가 발생한다. 동기화 문제는 공유영역을 순서대로 접근하면 해결된다.

이 예제에서도 하나 이상의 쓰레드가 공유영역을 접근함으로써 동기화 문제가 발생한다. 이 예제에서는 쓰레드 간에 ③ 부분과 ③ 부분, ③ 부분과 ④ 부분, ④ 부분과 ④ 부분이 겹치지 않고 순서대로 수행이 되어야 동기화 문제가 발생하지 않는다.

이 예제에서 발생한 동기화의 문제는 다음과 같이 세마포어를 이용해 문제를 해결할 수 있다. 세마포어에 대한 구체적인 설명과 사용법은 나중에 다루기로 한다. 여기서는 겹치면 안되는 부분의 처음과 마지막 부분을 세마포어로 보호해주면 된다 하는 정도로 알고 넘어가기로 한다. 다음 예제에서 음영이 들어간 부분이 추가된 부분이다. main() 함수내의 sem_init() 함수는 for 문 바로 앞에 추가한다.

사용자 삽입 이미지


여러 차례 실행하더라도 global_counting 변수 값과 sum_local_counting 변수 값이 똑같이 0x10000000이 나온다. 주의할 점은 수행시간이 많이 길어진다.

이상에서 우리는 쓰레드 프로그램에서의 동기화 문제와 그에 대한 해결책을 보았다. 이러한 동기화의 문제는 리눅스 커널에서도 발생할 수 있다. 우리가 작성하는 디바이스 드라이버는 리눅스 커널의 주요한 여러 흐름(시스템 콜 영역, top half 영역, bottom half 영역)의 부분으로 동작하며 따라서 디바이스 드라이버 내에서도 여러 가지 동기화 문제가 발생할 수 있다.


디바이스 드라이버의 주요한 동작과 리눅스 커널의 흐름에서의 디바이스 드라이버의 위치

다음은 디바이스 드라이버의 주요한 동작과 이러한 동작들이 커널의 어떤 흐름에서 이루어지는지 알아보자.

디바이스 드라이버의 주요한 동작은 크게 세가지로 나눌 수 있다.

첫번째는 [디바이스에 쓰기 동작]이다. [디바이스에 쓰기 동작]의 경우 시스템 콜을 통해서 디바이스에 쓰고자 하는 데이터를 쓴다. 이 동작을 통하여 하드 디스크나 네트워크 카드등에 데이터를 쓴다. [디바이스에 쓰기 동작]과 관련한 커널의 흐름은 다음과 같다.

* 시스템 콜 루틴 내부:
디바이스가 멈추어 있을 경우 데이터를 디바이스 버퍼에 쓰고 나간다
디바이스가 동작중일 경우 데이터를 데이터 큐에 넣고 나간다

* 하드웨어:
디바이스가 데이터를 다 보냈다 -> hardware interrupt 발생

* top half 루틴 내부:
bottom half 요청

* bottom half 루틴 내부:
데이터 큐가 비어 있으면 그냥 나간다
데이터 큐가 비어 있지 않으면 데이터를 하나 꺼내서 디바이스 버퍼에 쓰고 나간다

두 번째는 <동기적으로 디바이스로부터 읽기 동작>이다. <동기적으로 디바이스로부터 읽기 동작>은 시스템 콜을 통해서 디바이스에 읽기를 요청한다. 디바이스에 읽기를 요청하면 어느 정도 시간이 흐른 후에 디바이스 내부 버퍼에 데이터가 도착하며 디바이스는 하드웨어 인터럽트를 이용하여 CPU에게 데이터의 도착을 알린다. 그러면 CPU는 인터럽트 핸들러를 통하여 이 데이터를 읽어간다. 하드 디스크나 CDROM으로부터 데이터를 읽어가는 동작이 이에 해당한다. <동기적으로 디바이스로부터 읽기 동작>과 관련한 커널의 흐름은 다음과 같다.

* 시스템 콜 루틴 내부:
디바이스가 멈추어 있을 경우 디바이스에 데이터 읽기를 요청하고 디바이스로부터 데이터 큐에 데이터가 도착하기를 기다린다
디바이스가 동작중일 경우 디바이스의 사용이 끝나기를 기다린다 (임의의 다른 프로세스가 디바이스를 사용 중이므로)

데이터 큐에서 데이터를 꺼낸다
디바이스의 사용이 끝났음을 알린다

* 하드웨어:
디바이스에 데이터가 도착했다 -> hardware interrupt 발생

* top half 루틴 내부:
메모리 버퍼를 하나 할당해 디바이스 버퍼로부터 데이터를 읽어 들인 후 메모리 버퍼를 데이터 큐에 넣는다
bottom half 요청

* bottom half 루틴 내부:
디바이스로부터 데이터 큐에 데이터가 도착했음을 알린다

세 번째는 <비동기적으로 디바이스로부터 읽기 동작>이다. <비동기적으로 디바이스로부터 읽기 동작>은 시스템 콜을 통해서 디바이스로부터 도착한 데이터를 읽고자 한다. 이 경우 데이터는 비동기적으로 디바이스에 도착하며, 인터럽트를 통해 데이터의 도착을 CPU에게 알린다. 그러면 CPU는 인터럽트 핸들러를 통하여 이 데이터를 읽어간다. 네트워크 카드나 시리얼 디바이스에 도착한 데이터를 읽어가는 동작이 이에 해당한다. <비동기적으로 디바이스로부터 읽기 동작>과 관련한 커널의 흐름은 다음과 같다.

* 시스템 콜 루틴 내부:
데이터 큐에 데이터가 있으면 데이터를 가져간다
데이터 큐에 데이터가 없으면 디바이스로부터 데이터 큐에 데이터가 도착하기를 기다린다

* 하드웨어:
디바이스에 데이터가 도착했다 -> hardware interrupt 발생

* top half 루틴 내부:
메모리 버퍼를 하나 할당해 디바이스 버퍼로부터 데이터를 읽어 들인 후 메모리 버퍼를 데이터 큐에 넣는다
bottom half 요청

* bottom half 루틴 내부:
디바이스로부터 데이터 큐에 데이터가 도착했음을 알린다

이상 디바이스 드라이버의 주요한 동작과 리눅스 커널의 흐름에서의 디바이스 드라이버의 위치를 살펴 보았다. 지금까지 살펴본 디바이스 드라이버에 동기화 문제가 어떻게 발생할지 또 어떻게 해결해야 할 지에 대해서는 다음 기사에 자세히 다루기로 한다.


nested interrupt와 process scheduling에 의한 리눅스 커널의 흐름

사용자 삽입 이미지

[그림 2] 리눅스 커널의 기본적인 동작


[그림 2]는 각각 system call에 의한 리눅스 커널의 동작, hardware interrupt에 의한 리눅스 커널의 동작, nested interrupt에 의한 리눅스 커널의 동작을 나타낸다. 각 동작에 대한 구체적인 내용은 본지 8 월 호 <리눅스 커널의 이해 ②> 기사의 [그림 8], [그림 9], [그림 17]을 참조하기 바란다. 참고로 리눅스 커널 버전은 2.5 이후 버전이다.

[그림 3]은 리눅스 커널 내에서 프로세스 스케쥴링이 있을 수 있는 지점을 나타낸다.

먼저 프로세스 스케쥴링이 어떤 경우에 있을 수 있는지 보기로 하자.

⒜는 hardware interrupt가 발생했을 때 프로세스 스케쥴링을 수행하는 경우이다. 프로세스 스케쥴링을 기준으로 보았을 때 hardware interrupt는 크게 두 가지로 나눌 수 있는데, 첫 번째는 timer device로부터 온 경우이고, 두 번째는 timer device를 제외한 나머지 device(예를 들어 하드 디스크나 이더넷 카드)로부터 온 경우이다.

timer device로부터 interrupt가 들어왔을 때 프로세스 스케쥴링을 수행하는 경우는 두 가지로 나눌 수 있다. 먼저 timer interrupt의 interrupt handler(top half)에서 현재 프로세스의 time slice 값을 하나 감소시키고 그 결과값이 0일 때 스케쥴링을 요청한다. 다음은 timer interrupt의 bottom half에서는 여러 가지 시간과 관련한 일들을 처리하며, 이러한 일들 중에는 시간과 관련한 조건을 기다리던 프로세스를 wait queue에서 꺼내 run queue로 넣는 일도 있다. 이런 경우 wait queue에서 run queue로 들어간 프로세스가 현재 프로세스보다 우선순위가 클 경우 스케쥴링을 요청한다.

그 외의 device로부터 interrupt가 들어올 경우에는 top half 또는 bottom half에서 그 device와 관련한 어떤 조건을 기다리는(예를 들어 그 device로부터 데이터가 도착하기를 기다리는) 프로세스를 wait queue에서 꺼내 run queue로 넣는 일이 있는데, 이 때 wait queue에서 run queue로 들어간 프로세스의 우선순위가 현재 프로세스보다 우선순위가 클 경우 스케쥴링을 요청한다.

⒝는 시스템 콜 영역을 수행하는 도중에 현재 프로세스로부터 어떤 조건을 기다리던 프로세스를 wait queue에서 꺼내 run queue로 넣는 일이 있는데, 이런 경우 wait queue에서 run queue로 들어간 프로세스가 현재 프로세스보다 우선순위가 크면 스케쥴링을 요청하는 경우이다.

⒞는 시스템 콜 영역을 수행하는 도중에 현재 프로세스를 진행하기 위해 필요한 어떤 조건 을 만족하지 못해 현재 프로세스를 논리적으로 더 이상 진행하지 못할 경우, 현재 프로세스 를 wait queue로 넣고 프로세스 스케쥴링을 수행하는 경우이다. 여기서는 현재 프로세스를 wait queue로 넣음으로써 현재 프로세스를 blocking 시킨다.

여기서 주의할 점은 ⒞의 경우는 현재 프로세스를 wait queue로 넣지만, ⒜와 ⒝의 경우는 현재 프로세스가 run queue에 그대로 남아있다. ⒞와 같은 형태의 프로세스 스케쥴링을 Direct invocation이라 하고, ⒜, ⒝와 같은 형태의 프로세스 스케쥴링을 Lazy invocation이라 한다.

⒟는 시스템 콜 영역을 수행하는 도중에 nested interrupt가 들어 왔을 때 수행하는 프로세스 스케쥴링이며, 스케쥴링을 수행하는 조건은 ⒜의 경우와 같다.

⒠, ⒡는 현재 프로세스에게 도착한 시그널을 처리하는 도중에 nested interrupt가 들어 왔을 때 수행하는 프로세스 스케쥴링이며, 스케쥴링을 수행하는 조건은 ⒜의 경우와 같다.

사용자 삽입 이미지

[그림 3] 리눅스 커널에서 프로세스 스케쥴링의 시작과 끝


[그림 3]을 통해 리눅스 커널 내에서 프로세스 스케쥴링이 어디서 시작해서 어디서 끝나는지 살펴 보자. 참고로 프로세스 스케쥴링에 대한 구체적인 내용은 본지 7 월호 <리눅스 커널의 이해 ①> 기사 내용을 참조하기 바란다.

어떤 프로세스의 a 지점에서 시작한 프로세스 스케쥴링은 임의의 다른 프로세스의 b, d, f, h, j, l 지점에서 끝날 수 있다. 마찬가지로 어떤 프로세스의 c, e, g, i, k 지점에서 시작한 프로세스 스케쥴링은 임의의 다른 프로세스의 b, d, f, h, j, l 지점에서 끝날 수 있다.

사용자 삽입 이미지

[그림 4] 프로세스 스케쥴링을 통한 프로세스간 전환


[그림 4]에서 ⒜와 ⒝는 각각 a 지점에서 시작한 프로세스 스케쥴링이 d 지점에서 끝나는 경우와, g 지점에서 시작한 프로세스 스케쥴링이 f 지점에서 끝나는 경우를 나타낸다. [그림 3]의 ⒜와 ⒝의 경우처럼 a, c, e, g, i, k 지점에서 시작한 프로세스 스케쥴링이 b, d, f, h, j, l 지점에서 끝나는 프로세스간 전환의 형태는 36 가지가 있을 수 있다.

[그림 4]의 ⒜와 ⒝를 통해서 우리는 프로세스의 흐름이 어떤 프로세스의 임의의 사용자 영역(프로세스 P1의 A 영역)에서 임의의 다른 프로세스의 임의의 사용자 영역(프로세스 P2 의 B 영역)으로 옮겨가는걸 볼 수 있다. 이와 같은 방식으로 프로세스의 흐름이 프로세스 P1의 사용자 영역에서 프로세스 P2의 사용자 영역으로, 또 프로세스 P2의 사용자 영역에서 프로세스 P3의 사용자 영역으로, …, 프로세스 Pn-1의 사용자 영역에서 프로세스 Pn의 사용자 영역으로 옮겨갈 수 있다. 즉, [그림 3]의 ⒜, ⒝와 같은 방식으로 프로세스의 흐름이 임의의 프로세스 P1의 사용자 영역에서 임의의 프로세스 Pn의 사용자 영역으로 옮겨갈 수 있다.

사용자 삽입 이미지

[그림 5] 프로세스 P1에서 프로세스 Pn으로의 전환


사용자 삽입 이미지

[그림 6] 프로세스 P1과 Pn의 같은 시스템 콜 영역의 접근




[그림 5]는 한 번 이상의 프로세스간 전환을 통해 임의의 프로세스 P1에서 임의의 프로세스 Pn으로 프로세스의 흐름이 옮겨갈 수 있음을 나타낸다.

[그림 6]은 임의의 프로세스 P1과 Pn이 각각 A와 B 영역에서 같은 시스템 콜 영역을 수행할 수 있음을 나타낸다. 우리가 작성하는 디바이스 드라이버의 일부는 시스템 콜 영역에서 동작을 하는데, 디바이스 드라이버를 작성할 때 동기화 문제를 고려하지 않을 경우 문제가 발생할 수 있다. [그림 6]은 [그림 5]의 한 예이다.

사용자 삽입 이미지

[그림 7] nested interrupt 와 process schedule에 의한 커널간 경쟁 상태



[그림 7]은 임의의 프로세스 P1이 시스템 콜 영역을 수행하는 도중에 nested interrupt가 발생하여 g 지점에서 프로세스 스케쥴링을 통해 임의의 프로세스 P2(여기서는 나타내지 않음)를 거쳐 임의의 프로세스 Pn으로 프로세스의 흐름이 옮겨가는 상황을 나타낸다. 이 경우 A와 B 영역이 같은 시스템 콜 영역이라 할 때 프로세스 P1와 프로세스 Pn은 시스템 콜 영 역에서 경쟁 상태가 될 수 있다. 이러한 경쟁 상태는 일반적으로 시스템에 논리적인 문제를 일으킨다.

[그림 6]과 [그림 7]에서 보듯이 nested interrupt와 process scheduling에 의해 리눅스 커널내에서는 커널 영역간에 여러 가지 경쟁 상태가 발생할 수 있으며, 이러한 경쟁 상태는 일반적으로 시스템을 멈추게 하는 등의 심각한 문제를 일으킨다.

앞에서도 말한 것처럼 우리가 작성하는 디바이스 드라이버는 시스템 콜 영역, top half 영역, bottom half 영역에서 모두 동작한다. 따라서 우리가 작성하는 디바이스 드라이버 내에서도 여러 가지 경쟁 상태가 발생할 수 있다.

이상에서 우리는 동기화 문제란 무엇인지, 디바이스 드라이버의 주요한 동작과 리눅스 커널의 흐름에서의 디바이스 드라이버의 위치, nested interrupt와 process scheduling에 의한 리눅스 커널의 흐름을 구체적으로 알아보았다.
다음 호에는 리눅스 디바이스 드라이버 작성시 Uni-Processor 또는 Multi-Processor 환경에 따라 발생할 수 있는 동기화 문제의 여러 가지 패턴을 살펴보고 그에 대한 해결책을 알아보기로 하자.


http://network.hanbitbook.co.kr/view.php?bi_id=1068
"Kernel" 카테고리의 다른 글
  • 리눅스 커널의 이해(5): 디바이스에 쓰기 동작에... (0)2007/05/10
  • 리눅스 커널의 이해(4): Uni-Processor & Multi-Pr... (0)2007/05/10
  • 리눅스 커널의 이해(3): 리눅스 디바이스 작성시... (0)2007/05/10
  • 리눅스 커널의 이해(2): 리눅스 커널의 동작 (0)2007/05/10
  • 리눅스 커널의 이해(1) : 커널의 일반적인 역할과... (0)2007/05/10
2007/05/10 10:18 2007/05/10 10:18
Posted by webdizen
Tags nested interrupt, process scheduling, race condition, Thread, 리눅스 커널
No Trackback No Comment

Trackback URL : http://www.webdizen.net/blog/trackback/2916

Leave your greetings.

[로그인][오픈아이디란?]

Unix & Linux/Kernel2007/05/10 10:09

리눅스 커널의 이해(2): 리눅스 커널의 동작

저자: 서민우
출처: Embedded World

1. 리눅스 커널의 기본적인 동작

이제 리눅스 커널이 어떻게 동작하는지 들여다 보자.
리눅스 커널은 그 소스량은 엄청나지만 역시 커널의 기본적인 동작은 우리가 지금까지 보아온 커널의 동작과 별로 다르지 않다. 덧붙이자면 다른 RTOS도 역시 마찬가지다.

system call에 의해 시작하는 리눅스 커널의 일반적인 동작

[그림 1]은 system call에 의해 시작하는 리눅스 커널의 일반적인 동작이다.

사용자 삽입 이미지

[그림 1] system call에 의한 리눅스 커널의 일반적인 동작


[그림 1]에서 커널은 process의 system call에 의해 수행을 시작한다. 먼저 커널의 시작 부분에서는 현재 process의 사용자 영역에서의 register의 내용을 stack상에 저장한다. 다음은 커널에서 사용자 영역으로 빠져 나가기 바로 전에 커널의 시작 부분에서 stack상에 저장한 register의 내용을 다시 복구한다. sys_func(), sys_func()내의 schedule(), sys_func()를 수행하고 난 후에 수행하는 schedule()의 역할은 전 월호의 [그림 5]에서 이미 설명했다. 리눅스 커널에서는 어떤 process에서 또 다른 process로, 또는 interrupt handler에서 process로 signal을 보낼 수 있으며, do_signal()에서는 커널영역에서 사용자 영역으로 빠져 나가기 전에 현재 process에 도착한 signal이 있는지를 검사하고 도착한 signal이 있으면 적절히 처리하는 부분이다. 마지막으로 a와 b사이에서는 기본적으로 hardware interrupt를 허용하며, 이 구간에서 발생하는 hardware interrupt를 일반적으로 nested interrupt라 한다. nested interrupt에 의해 수행을 시작하는 커널을 우리는 nested interrupt routine이라고 하며, 일반적으로 nested interrupt routine에 의해 커널의 흐름은 상당히 복잡해지며, 여러 가지 동기화 문제가 발생한다. nested interrupt routine에 의해 발생하는 이러한 문제점과 그에 대한 해결책은 다음 기사에서 자세히 다루기로 하겠다.

hardware interrupt에 의해 시작하는 리눅스 커널의 일반적인 동작

[그림 2]는 hardware interrupt에 의해 시작하는 리눅스 커널의 일반적인 동작이다.

사용자 삽입 이미지

[그림 2] hardware interrupt에 의한 리눅스 커널의 일반적인 동작


[그림 2]에서 커널은 hardware interrupt에 의해서 수행을 시작한다. 리눅스 커널에서는 top_half()와 bottom_half()를 do_IRQ()라는 함수 내에서 차례로 수행한다. 다른 부분의 역할은 이미 전 월호의 [그림 2]와 앞의 [그림 1]에서 설명하였다. 한가지 짚고 넘어갈 점은 a와 b사이에서는 기본적으로 hardware interrupt를 허용한다. 따라서 이 구간에서도 역시 nested interrupt가 발생할 수 있다.

2. nested interrupt와 리눅스 커널의 동작

[그림 1]과 [그림 2]에서 우리는 리눅스 커널내에서 nested interrupt가 발생할 수 있는 영역을 보았다(각각 a와 b사이의 구간). nested interrupt에 의해 커널의 동작이 어떻게 바뀌는지 보기 전에 먼저 몇 가지 짚고 넘어갈 사항이 있다.

리눅스 커널내에서 각 영역의 속성과 우선 순위

[그림 1]에서 sys_func()은 커널이 process의 요청에 의해 수행하는 부분으로 process와 직접적으로 관련된 함수이다. do_signal()도 process와 직접적으로 관련된 함수이다. schedule() 역시, 새로 수행할 process를 runqueue로부터 뽑고(리눅스 커널에서는 ready queue를 runqueue라고 한다), 현재 process의 커널 영역에서의 register의 내용을 메모리에 저장하고, 새로 수행할 process의 커널 영역에서의 register의 내용을 메모리로부터 복구하는, process와 간접적으로 관련된 함수이다. save register와 restore registerprocess의 사용자 영역에서의 register의 내용을 메모리에 저장하고, 사용자 영역에서의 register의 내용을 메모리로부터 복구하는 동작으로 process와 관련된 부분이다.

[그림 2]에서 do_IRQ()는 커널이 device로부터 들어온 요청을 처리하는 부분이다. 그 중에 top_half()는, 예를 들어 device를 접근하는 등의, 시간상으로 신속히 처리해야 할 부분이며, bottom_half()는, 예를 들어 device로부터 메모리로 읽어온 data(top_half()에서 device로부터 메모리로 가져온)를 처리하는 등의, top_half()에 비해 비교적 천천히 처리해도 되는 부분이다. 나머지 schedule(), do_signal(), save register, restore register는 앞의 경우처럼 process와 관련된 부분이다.

이상에서 리눅스 커널 영역은 논리적으로 다음과 같이 세 부분으로 나눌 수 있다.

device와 직접적으로 관련된 top_half() 부분
device와 간접적으로 관련된 bottom_half() 부분
process와 직접적으로 또는 간접적으로 관련된
schedule(), sys_func(), do_signal(), save register, restore register 부분

처음에 리눅스 커널을 설계하는 과정에서 top_half(), bottom_half(), process와 관련된 함수들 순으로 우선 순위를 주었다. 우선 순위에 따라 커널 영역을 빨강, 녹색, 파랑으로 표시할 경우 [그림 3]과 같다. [그림 3]에서 save register와 restore register는 process와 관련된 부분이기는 하지만 nested interrupt가 발생할 수 없는 영역이므로 여기서는 색깔로 표시하지 않았다.

사용자 삽입 이미지

[그림 3] 리눅스 커널내에서 각 영역의 우선 순위


그러면 지금부터 nested interrupt에 의해 커널이 수행해야 할 동작이 어떻게 바뀌어야 할지 생각해 보기로 하자. 참고로 [그림 3]에서 커널 영역 중 색깔이 없는 부분에서는 interrupt를 허용하지 않는다고 가정하자.

top_half()와 nested interrupt routine

먼저 top_half() 부분에서 interrupt가 발생했을 경우를 생각해 보자. 리눅스 커널에서는 top_half() 부분에서 interrupt handler에 따라 interrupt를 막을 수도 있고 열어 놓을 수도 있다. 이 부분에서 interrupt를 열어 놓아 interrupt가 발생하였을 경우에 리눅스 커널은 [그림 4]와 같이 동작해야 한다.

사용자 삽입 이미지

[그림 4] top_half()와 nested interrupt routine


[그림 4]에서 A와 B의 우선순위는 같다 하더라도 A에서 interrupt를 허용하였기 때문에 A를 수행하는 중이라도 B는 수행이 될 수 있다. 그러나, C, D, E는 A보다 우선순위가 낮기 때문에 수행하지 않고 나가는 것이 논리적으로 맞다. 그럼 리눅스 커널은 C, D, E를 수행하지 않는가? 그건 아니다. C의 경우 F를 수행할 때 함께 처리한다. D의 경우는 B나 C에서 schedule을 요청할 경우 수행하는 부분으로 G에서 처리하면 된다. E의 경우는 현재 process에게 도착한 signal을 처리하는 부분이며, H와 중복된다. 따라서 H에서 처리하면 된다.

bottom_half()와 nested interrupt routine

다음은 bottom_half()에서 interrupt가 발생했을 경우를 생각해 보자. 앞에서 bottom half()에서는 기본적으로 interrupt가 열려 있다고 말한 바 있다. 이 부분에서 interrupt가 들어올 경우 커널은 [그림 5]와 같이 동작해야 한다. [그림 5]에서 B의 우선 순위는 F의 우선 순위보다 크다. 따라서, F를 수행하는 도중이라도 B를 수행할 수 있다. C의 경우는 F와 우선 순위가 같으므로 B 다음에 바로 처리하지 않고, F를 처리한 후에, F를 다시 수행하여 C를 처리한다. D, E에 대한 처리는 [그림 4]에서 이미 설명하였다.

사용자 삽입 이미지

[그림 5] bottom_half()와 nested interrupt routine


schedule()과 nested interrupt routine

다음은 schedule()을 수행하는 중에 interrupt가 발생했을 경우를 생각해 보자. 이 부분에서 interrupt가 들어올 경우 리눅스 커널은 [그림 6]과 같이 동작해야 한다.

사용자 삽입 이미지

[그림 6] schedule()과 nested interrupt routine


[그림 6]에서 A는 process와 관련된 부분으로 B와 C보다 우선순위가 낮다. 따라서 A를 수행하는 중이라도 커널은 B와 C를 당연히 수행해야 한다. D는 A와 같은 부분으로 A와 우선 순위가 같다. 따라서 A를 수행하고 나서, A를 다시 한 번 더 수행하면 된다. 즉 nested interrupt routine에서는 D를 수행할 필요가 없다. E는 앞에서 설명한 것처럼 H와 중복되므로 수행할 필요가 없다.

do_signal()과 nested interrupt routine 그리고 커널 preemption

다음은 do_signal()을 수행하는 중에 interrupt가 발생했을 경우를 생각해 보자. 이 부분에서 interrupt가 들어올 경우 리눅스 커널은 [그림 7]과 같이 동작하도록 설계되었다.

사용자 삽입 이미지

[그림 7] do_signal()과 nested interrupt routine


[그림 7]에서 A는 process와 관련된 부분으로 B와 C보다 우선순위가 낮다. 따라서 A를 수행하는 중이라도 커널은 당연히 B와 C를 수행해야 한다. B나 C에서 wait queue에 있던 process를 runqueue에 넣고, runqueue에 새로 들어간 process가 현재 process보다 우선 순위가 클 경우 process scheduling을 요청할 수 있다. 그러면 커널은 D를 수행하며 A를 수행중이었더라도 다른 process로 전환이 일어나게 된다. 이는 A가 커널의 한 영역이라도 process와 관련된 부분이므로, A와 관련된 현재 process보다 우선 순위가 큰 process가 B나 C에서 runqueue로 들어갈 경우 당연히 process 전환을 수행할 수 있다. 이는 리눅스 커널 2.5 이후에 새로이 추가된 기능으로 커널 preemption이라고 한다. 당연히 리눅스 커널 2.4에는 없는 기능이다. E는 A와 중복되므로 수행하지 않는다.

sys_func()과 nested interrupt routine 그리고 커널 preemption

다음은 sys_func()를 수행하는 중에 interrupt가 발생했을 경우를 생각해 보자. 이 부분에서 interrupt가 들어올 경우 리눅스 커널은 [그림 8]과 같이 동작하도록 설계되었다.
[그림 8]에서 A(sys_func())는 process와 관련된 부분으로 [그림 7]에서의 A(do_signal())와 같이 취급한다. 당연히 [그림 8]에서 A를 수행하는 도중이라도 B와 C를 수행해야 하며, 필요 시에는 D에 의해 다른 process로 전환할 수 있다. 이 역시 리눅스 커널 2.5 이후에 새로이 추가된 커널 preemption 기능이다. E는 F와 중복되므로 수행하지 않는다.

사용자 삽입 이미지

[그림 8] sys_func()과 nested interrupt routine


sys_func()내의 schedule()과 nested interrupt routine

다음은 sys_func()내에서 schedule()을 수행하는 중에 interrupt가 발생했을 경우를 생각해 보자. 이 부분에서 interrupt가 들어올 경우 리눅스 커널은 [그림 9]와 같이 동작하도록 설계되었다.

사용자 삽입 이미지

[그림 9] sys_func()내의 schedule()과 nested interrupt routine


[그림 9]에서 A는 process와 관련된 작업이다. 따라서 A를 수행하는 도중이라도 당연히 B와 C를 수행해야 한다. D는 A와 같은 부분으로 A와 우선 순위가 같다. 따라서 A를 수행하고 나서 수행해야 한다. 즉 nested interrupt routine에서는 수행할 필요가 없다. E는 앞에서 설명한 것처럼 F와 중복되므로 수행할 필요가 없다.

nested interrupt에 의한 커널의 동작

이상에서 우리는 nested interrupt에 의해 커널 수행해야 할 동작을 보았으며, [그림 10]과 같다.

사용자 삽입 이미지

[그림 10] nested interrupt에 의한 커널의 동작


지금까지 우리는 리눅스 커널이 실제로 어떻게 설계되었는지 보았다.

3. multi-tasking의 구현

다음은 간단한 scheduling과 context switching에 의해 multi-tasking이 어떻게 구현되는지를 보여주는 예다. 이 예를 통해서 마술 같은 multi-tasking을 구체적으로 이해해 보기로 하자. 지난 기사에서 설명한 부분에 대한 이해를 돕고자 이 부분을 추가하였다.

먼저 scheduling이란 현재 process를 어떤 이유에 의해서 잠시 멈출 때 새로이 수행할 process를 선택하는 커널의 동작을 말한다.

다음으로 context란 processor(CPU)가 어떤 process를 수행할 때의 processor의 상태를 말한다. processor의 상태란 구체적으로 processor 내의 여러 register의 어느 순간의 상태를 말한다. 따라서 context switching이란 현재 수행하던 process의 context를 그대로 메모리로 저장하며, scheduling을 통해 선택한 새로운 process의 context를 메모리로부터 processor의 여러 register로 복구하는 커널의 동작을 말한다.

다음의 예는 multi-tasking.s와 multi-tasking.c의 두 가지 파일로 구성된다. 그럼 구체적으로 구현 내용을 들여다 보자.





<1>에서 process_state는 하나의 process를 관리하기 위한 구조체이다. 이 구조체 내의 stack_top 변수는 stack pointer를 저장하기 위한 공간이고, stack 배열 변수는 256*4 byte 크기의 process stack이다.

<2>에서는 두 개의 process를 관리하기 위하여 process_state 구조체 두 개를 process 배열 변수로 선언하였다.

<3>에서는 scheduling과 context switching시 사용할 process_state 구조체를 가리킬 수 있는 pointer 변수 두 개를 선언하였다.

<4>에서 <5>까지는 process[OTHER]의 상태를 초기화 하며, process[OTHER]의 상태는 [그림 11]과 같아진다.
사용자 삽입 이미지
사용자 삽입 이미지

[그림 11] process[OTHER]의 초기화


<6>은 간단하지만 새로운 작업을 선택하는 scheduling 과정이다. 여기서는 새로운 작업으로 process[OTHER]를 선택한다.

<7>을 어셈블리어로 나타내면 다음과 같다.

사용자 삽입 이미지



여기서 과 , 과 부분에서 스택에 차례로 next, prev 값이 들어간다. 그리고, 부분에서 스택에 return address(0x0804852d) 값이 들어가며, multi-tasking.s 파일의 context_switch 함수로 뛴다. [그림 12]에서 ① 부분이 이 과정에서 만들어진다.

다음은 multi-tasking.s 파일의 context_switch 함수를 보자.

먼저 ⓐ에서 [그림 12]의 ② 부분이 만들어진다. 다음으로 ⓑ에서 processor의 esp register 값([그림 12]의 ③)을 process[MAIN]의 stack_top([그림 12]의 ④)에 저장한다. 이로써 지금까지 수행하던 process의 문맥 저장을 끝낸다.

다음은 ⓒ에서 process[OTHER]의 stack_top 값([그림 12]의 ⑤)을 processor의 esp register([그림 12]의 ⑥)에 저장한다. 이 부분에서 esp register는 process[OTHER]의 stack top을 가리킨다. process[OTHER]는 이전에 초기화 되었으며, 이미 [그림 11]에서 살펴 보았다. ⓓ에서 ⑦부분에 저장된 값들이 processor의 각 register로 채워진다. 이로써 새로 수행할 process의 문맥을 복구하였다. 마지막으로 ret 명령에 의해 [그림 12]의 ⑧에서 ra의 값이 eip로 들어가면서 other 함수를 수행하기 시작한다. 이 때 esp register는 [그림 12]의 ⑨를 가리킨다.

사용자 삽입 이미지

[그림 12] process간 전환


multi-tasking의 동작 방식을 이해했으면 마지막으로 두 파일을 컴파일 하여 실행해 본다.

이상에서 우리는 multi-tasking이 어떻게 구현되는지를 보았다. 비록 짧은 소스이기는 하지만 중요한 개념들이 많이 들어가 있으며, 커널의 핵심적인 부분만을 떼내어 이해할 수 있다.

마무리

지금까지 우리는 리눅스 커널이 어떻게 설계되었는지 보았다. 또한 multi-tasking이 어떻게 구현되는지 보았다. 이 과정에서 scheduling과 task 초기화도 들여다 보았다. 다음 기사에서는 리눅스 커널이 구체적으로 어떻게 구현되었는지 소스 수준에서 살펴 보기로 하자.
"Kernel" 카테고리의 다른 글
  • 리눅스 커널의 이해(4): Uni-Processor & Multi-Pr... (0)2007/05/10
  • 리눅스 커널의 이해(3): 리눅스 디바이스 작성시... (0)2007/05/10
  • 리눅스 커널의 이해(2): 리눅스 커널의 동작 (0)2007/05/10
  • 리눅스 커널의 이해(1) : 커널의 일반적인 역할과... (0)2007/05/10
  • Kprobes를 이용한 커널 디버깅 (0)2007/05/04
2007/05/10 10:09 2007/05/10 10:09
Posted by webdizen
Tags hardware interrupt, multi tasking, nested interrupt, system call, 리눅스 커널
No Trackback No Comment

Trackback URL : http://www.webdizen.net/blog/trackback/2915

Leave your greetings.

[로그인][오픈아이디란?]

«Prev  1  Next»

RSS HanRSS
Blog Image
webdizen
이곳은 컴퓨터에 대해 연구하고, 공유하고, 소통하기 위한 연구실입니다. 개인적으로는 OLAP, Data Mining, Semantic Web, Data Modeling에 대해서 연구하고 있습니다.

Categories

전체 (3009)
Webdizen (141)
Life (6)
Diary (16)
Blog (9)
IDEA (2)
Travel (10)
Book (16)
Photo (7)
Movie (8)
Music (14)
Leisure Sports (10)
Funny (6)
Hardware (121)
Software (120)
Windows (5)
Unix & Linux (120)
Installation (5)
Kernel (10)
System (34)
Develop (22)
X-Window (0)
Applicaton (31)
Security (4)
Framework (2)
Hadoop (2)
Programming (804)
Algorithm & Data Structure (1)
Assembly (38)
UNIX/Linux C (95)
C++ (128)
STL (4)
Java (38)
Win32 API (92)
ATL/COM (44)
MFC (151)
.NET (26)
WCF/WPF (4)
C# (28)
Network Programming (17)
Database Programming (12)
OpenGL / DirectX (13)
Multimedia Programming (0)
Game Programming (21)
Parallel Distributed Progra... (0)
Reverse Engineering (0)
Debugging (9)
Python (1)
Ruby (1)
Ruby on Rails (1)
QT (4)
GTK (0)
JSP (0)
PHP (6)
ASP.NET (6)
ASP (2)
Development (28)
Useful Library (2)
Data Modeling (0)
Database (105)
Oracle (4)
MSSQL (41)
MySQL (2)
Data Warehouse (2)
Data Mining (4)
Network (66)
Web (79)
DHTML (4)
XHTML (1)
Javascript (1)
CSS (1)
AJAX (9)
XML (11)
Flex (1)
Silverlight (3)
Security (91)
DoS (1)
Kernel (10)
Scanning (3)
Sniffing (0)
Spoofing (4)
Overflow (28)
Web (11)
Shell (10)
Format String (14)
Window (2)
Embedded (70)
Multimedia (27)
Mobile (14)
Graphic (24)
Management (633)
Knowledge (581)
Hadoop (0)

Notice

  • 메타 블로그 사이트에 등록
  • 새해 맞이 블로그의 변화
  • 블로그 명칭 변경
  • 도메인(www.webdizen.net) 구...
  • TEXTCUBE 1.6.1로 업그레이드...

Tags

  • BDM
  • Foundstone
  • 한국과학기술원
  • 보드카
  • 디스크 복사
  • 함수 개체
  • SASS
  • Worker
  • 순위
  • Kernel
  • putty
  • 1970년대
  • 병렬화
  • Uni-Processor
  • 네트워크 라인
  • Hash-Join
  • Windows Fresentation Foundation
  • 습관
  • Regular Expression
  • 이건희

Recent Articles

  • 트위터(Twitter)의 시작!.
  • 청년 리더의 조건.
  • 애플의 타블렛 PC - 아이패드....
  • 미래의 인터페이스 - 육감 기....
  • 기초발성법 동영상 강좌.

Recent Comments

  • 학교 과제물중 쓰레드에 대하....
    장진혁 03/17
  • 관리자만 볼 수 있는 댓글입....
    비밀방문자 03/12
  • 상대방의 이야기를 열심히 경....
    DoNuts 03/03
  • Lots of students know techn....
    Bobbi35Shannon 02/25
  • 좋은글 잘 보고 갑니다..
    Und_hacker 01/08

Recent Trackbacks

  • printf,scanf를 이용한 형식....
    yundream의 프로그래밍 이야기 03/10
  • 파일 열기/저장하기 CFileDialog.
    은마군의 나태블록 2009
  • World IT Show 2008.
    상우 :: Oranzie's BLOG 2008
  • cvs서버 설치하기.
    3인3색 2008
  • 속속 공개되는 Google Chart....
    PHP와 Web 2.0 2007

Archive

  • 2010/02 (1)
  • 2010/01 (6)
  • 2009/12 (5)
  • 2009/09 (3)
  • 2009/08 (1)

Calendar

«   2010/03   »
일 월 화 수 목 금 토
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31      

Bookmarks

    • Administration
      • IIS.NET
      • NTFAQ
      • OS의 모든 것
      • 리눅스포털
    • Database
      • SQL Server Central
      • SQL Team
    • Development
      • .NET Heaven
      • ASP Alliance
      • ASP.NET 2.0
      • Bullog.net
      • C# Corner
      • C++ (C PlusPlus.com)
      • C++ Reference
      • CodeGuru
      • CodePlex
      • DebugLab
      • Dev Articles
      • Devpia
      • DotNet Junkies
      • DotNet Zone
      • Driver Online
      • GOSU.NET
      • HOONS 닷넷
      • Joinc 팀블로그
      • KOSR
      • MSDN Home Page
      • OSR Online
      • Sky.ph - 개발자 커뮤니...
      • TAEYO.NET
      • The Code Project
      • WindowsClient.net
      • 김상욱의 개발자 Side
      • 조인시 위키
    • Human Networks
      • belief21c's e-space
      • I think I can
      • Invisible Rover's Blog :D
      • Rodman®
      • ■ Feel So Good~! ■
      • 까만 나비
      • 나를 가꾸는 시간.
      • 나만의 즐거움~~!
      • 단녕
      • 상우 :: Oranzie's BLOG
    • Information Technology
      • Microsoft TechNet
      • 지디넷코리아 - 글로벌...
    • Security
      • FoundStone
      • milw0rm
      • NewOrder
      • OpenRCE
      • Phrack.org
      • Reverse Engineering b1...
      • Reverse Engineering Team
      • RootKit
      • SecurityFocus
      • SecurityXploded by Nag...
      • Wow Hacker
      • Zone-H
Textcube
Louice Studio Inc.
Powered by Textcube. Original designed by Tistory.