수안이의 컴퓨터 연구실

  • Mainpage
  • About Me
  • Tags
  • Metapage
  • Notice
  • Location
  • Keywords
  • Guestbook
  • Admin
  • Write an Article
  • Total | 1691259
  • Today | 565
  • Yesterday | 628

8 Articles, Search for '리눅스 커널'

  1. 2008/01/20 눈길가는 블로그 아티클 [1월 셋째주]
  2. 2007/05/10 리눅스 커널의 이해(5): 디바이스에 쓰기 동작에 대한 구체적인 작성 예
  3. 2007/05/10 리눅스 커널의 이해(4): Uni-Processor & Multi-Processor 환경에서의 동기화 문제
  4. 2007/05/10 리눅스 커널의 이해(3): 리눅스 디바이스 작성시 동기화 문제
  5. 2007/05/10 리눅스 커널의 이해(2): 리눅스 커널의 동작
  6. 2007/05/10 리눅스 커널의 이해(1) : 커널의 일반적인 역할과 동작
  7. 2007/05/04 왜 리눅스 커널을 알아야 하는가?
  8. 2006/12/27 리눅스 커널 코딩 스타일
Webdizen/Blog2008/01/20 23:49

눈길가는 블로그 아티클 [1월 셋째주]

MySQL, Sun에 팔리다.
Sun이 MySQL을 산답니다!
Sun Microsystems, MySQL AB 10억 달러에 인수
Sun이 MySQL을 사버렸군요!
Sun의 오픈소스 진영으로의 변화에 이어서 정말 놀라운 일이 계속 일어나네요.

결국, BEA Oracle 에 인수 되버리다
Oracle... BEA... 엄청난 액수에 인수해버리는군요.

2007년 인상깊은 건축물 Top 100
이런 건축물들을 볼때마다 사람들의 무한한 능력을 다시 느끼게 된답니다.

음악 치료에 응용되는 음악
평소 클래식 음악을 즐겨 듣는 편입니다. 다들 들었을 베토벤과 모짜르트 음악이죠;
이런 정보도 괜찮네요. 한번 찾아서 들어봐야겠습니다.

매일 아침 고구마 한개 껍질째 드세요
고구마를 좋아해서 아침마다 쪄 먹고 그랬는데; 껍질째 먹으라네요; ㅋㅋ
껍질 깨끗이 씻고, 통째로 먹어야 겠습니다.

에어론 의자 (Herman Miller, Aeron Chair)
구름 위에 앉은 걸까?
사실 연구실에 의자들을 모두 바꿔보려고 의자를 찾아보다가...
이럴수가; 100만원이 넘는 놈이네요; 에어론 의자! 돈만 있다면 사고 싶어지네요;

25 Beautiful Minimalistic Website Designs
정말 아름다운 웹 사이트 25개이네요. 웹 디자인에도 관심이 많아서; ㅋㅋ

Ruby on the Rails를 닮은 PHP 프레임워크, CakePHP
저번에 Web Framework에 대해서 조사하다가 잠깐 봤었던 CakePHP Framework 였는데...
멋진 기능들이 상당하네요... 나중에 한번 써봐야겠습니다.

사이클로이드(cycloid) 블로깅
억지로 해서 안되는 일도 시간이 지나면 저절로 해결되는 경험은 누구나 가지고 있을 것입니다. 아무리 머리를 짜내도 떠오르지 않던 해답이 어느 순간 저절로 생각나는 경우도 있었을 것입니다. 밤새워가며 한일인데 한숨자고 일어나 다시 보니 헛점투성이라서 당황했던 기억이 있을 것입니다. 시간이라는 기한에 쫓겨서 서두른 일은 늘 빈틈을 남기고, 오히려 그르친 결과로 이어지기도 합니다. 급할수록 돌아가라는 말은 이런 일상의 경험에서 나온 격언이기도 하지만 매우 과학적인 근거를 가지고 있습니다. 시작과 끝을 연결하는 최단거리의 직선보다 끝지점에 더 빨리 도착하는 사이클로이드(cycloid)는 더 긴 거리를 지닌 곡선입니다.

저는 이와 같은 경험을 많이 가지고 있었는데... 과학적인 근거가 확실히 있었군요.

카지노에서 돈 따는 법
일단 Las Vegas에 갈때를 위한 단순 참고용

리눅스 커널(kernel)에 심각한 보안 결함 발견
이거 커널 업데이트 해야 할 서버가 한두개가 아닌데; 큰일이네요;
그 전에 보안 결함 이용하는 방법 좀 알고 싶어지네요;

웹 개발자 VS 웹 서비스 기획자
흥미있는 글입니다.

WPF를 이용한 멋진 '뉴스리더' 체험해 보세요
오호! 재미있습니다.

내 블로그의 정보를 파해친다!! IWEBTOOL.COM
이거 유용하네요. ^^
"Blog" 카테고리의 다른 글
  • 나만의 블로그 명함 탄생! (3)2008/04/19
  • 눈길가는 블로그 아티클 [1월 넷째주] (0)2008/01/30
  • 눈길가는 블로그 아티클 [1월 셋째주] (0)2008/01/20
  • 눈길가는 블로그 아티클 [1월 둘째주] (0)2008/01/13
  • 눈길가는 블로그 아티클 [1월 첫째주] (0)2008/01/06
2008/01/20 23:49 2008/01/20 23:49
Posted by webdizen
Tags BEA, CakePHP, Framework, MySQL, Oracle, Ruby, Ruby on the Rails, Sun, Web Tool, Website, WPF, 건축물, 고구마, 뉴스리더, 리눅스 커널, 보안 결함, 사이클로이드, 에어론 의자, 웹 개발자, 웹 서비스 개발자, 음악 치료, 카지노, 프레임워크
No Trackback No Comment

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

Leave your greetings.

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

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

리눅스 커널의 이해(5): 디바이스에 쓰기 동작에 대한 구체적인 작성 예

저자: 서민우
출처: Embedded World

[ 관련 기사 ]
♠ 리눅스 커널의 이해(1) : 커널의 일반적인 역할과 동작
♠ 리눅스 커널의 이해(2): 리눅스 커널의 동작
♠ 리눅스 커널의 이해(3): 리눅스 디바이스 작성시 동기화 문제
♠ 리눅스 커널의 이해(4): Uni-Processor & Multi-Processor 환경에서의 동기화 문제

이번 기사에서는 [디바이스에 쓰기 동작]에 대한 구체적인 작성 예를 살펴보고, 동기화 문제에 대한 처리를 적절히 해 주지 않을 경우 어떤 문제가 발생하는지 보기로 하자. 또한 지난 기사에서 살펴 보았던 동기화 문제에 대한 해결책을 이용하여 발생하는 문제점을 해결해 보기로 하자.

다음은 [디바이스에 쓰기 동작]을 중심으로 작성한 리눅스 디바이스 드라이버의 한 예다. 여기서는 독자가 모듈 형태의 리눅스 디바이스 드라이버를 작성할 줄 알고, 동적으로 리눅스 커널에 모듈을 삽입할 줄 안다고 가정한다.



그러면 동기화 문제와 관련한 부분을 중심으로 소스를 살펴 보자.

dev_write 함수는 write 시스템 콜 함수에 의해 시스템 콜 루틴 내부에서 수행된다. dev_write 함수에서 ①, ②, ③ 부분은 논리적으로 다음과 같다.


디바이스를 사용하고 있지 않으면
디바이스를 사용한다고 표시하고
데이터를 디바이스 버퍼에 쓰고 나간다


③ 부분에서 dev_buffer 변수는 가상 디바이스의 버퍼를 나타낸다. 그리고 ①과 ② 부분에서 사용한 dev_key 변수는 가상 디바이스의 버퍼를 하나 이상의 프로세스가 동시에 접근하지 못하게 하는 역할을 한다.

우리는 전월 호에서 이와 같은 루틴에서 발생하는 동기화 문제를 다음과 같이 처리할 수 있음을 보았다.


cli
디바이스를 사용하고 있지 않으면
디바이스를 사용한다고 표시하고
데이터를 디바이스 버퍼에 쓰고 나간다
sti


리눅스 커널에는 cli와 sti에 해당하는 local_irq_save와 local_irq_restore라는 매크로가 있다. 이 두 매크로를 이용하여 dev_write 함수의 ①, ②, ③ 부분에서 발생할 수 있는 동기화 문제를 다음과 같이 처리할 수 있다.





여기서 local_irq_save(flags) 매크로는 CPU 내에 있는 flag 레지스터를 flags 지역 변수에 저장한 다음에 인터럽트를 끄는 역할을 한다. local_irq_restore(flags) 매크로는 flags 지역 변수의 값을 CPU 내에 있는 flag 레지스터로 복구함으로써 인터럽트를 켜는 역할을 한다.

또 dev_write 함수에서 ①과 ④ 부분은 논리적으로 다음과 같다.


디바이스를 사용하고 있으면
데이터를 데이터 큐에 넣고 나간다


④ 부분에서 data_slot 배열 변수는 원형 데이터 큐를 나타낸다. empty_slot_pos 변수는 데이터를 채워 넣어야 할 큐의 위치를 나타낸다. empty_slot_num 변수는 큐의 비어 있는 데이터 공간의 개수를 나타낸다. 그래서 큐에 데이터를 채워 넣기 전에 empty_slot_num 변수의 값을 하나 감소시킨다. full_slot_num 변수는 큐에 채워진 데이터 공간의 개수를 나타낸다. 그래서 큐에 데이터를 채워 넣은 후에 full_slot_num 변수의 값을 하나 증가시킨다.

우리는 전월 호에서 이와 같은 루틴에서 발생하는 동기화 문제를 다음과 같이 처리할 수 있음을 보았다.


cli
디바이스를 사용하고 있으면
데이터를 데이터 큐에 넣고 나간다
sti


따라서 dev_write 함수의 ①과 ④ 부분에서 발생할 수 있는 동기화 문제를 다음과 같이 처리할 수 있다.





⑤ 부분은 ③ 부분에서 디바이스 버퍼에 데이터를 쓰고 나면, 디바이스가 동작하기 시작함을 논리적으로 나타낸다.

사용자 삽입 이미지

[그림 1] 디바이스에 쓰기 예


앞에서 우리는 ③ 부분에서 가상 디바이스의 버퍼를 사용한다고 했다. 따라서 이 디바이스에 의한 hardware interrupt는 발생할 수 없다. 그래서 여기서는 주기적으로 발생하는 timer interrupt를 가상 디바이스에서 발생하는 hardware interrupt라고 가정한다. 그럴 경우 timer interrupt는 [그림 1]과 같이 발생할 수 있으며, 이 그림은 전월호의 [그림 2]와 논리적으로 크게 다르지 않음을 볼 수 있다.

[그림 1]에서 ⓐ 부분은 dev_working 함수의 ⑥ 부분을 나타낸다. 여기서는 dev_interrupt 구조체 변수의 멤버 변수인 expires 변수 값을 커널 변수인 jiffies 변수 값에 1을 더해서 설정한다. jiffies 변수는 커널 변수로 주기적으로 발생하는 timer interrupt를 처리하는 루틴의 top_half 부분에서 그 값을 하나씩 증가시킨다. 리눅스 커널 버전 2.6에서는 초당 1000 번 timer interrupt가 발생하도록 설정되어 있다.

[그림 1]의 ⓑ 부분에서는 jiffies 변수 값을 증가시키고 있다. jiffies 변수 값을 증가시키는 함수는 do_timer 함수이며, timer interrupt handler 내에서 이 함수를 호출한다. do_timer 함수는 리눅스 커널 소스의 linux/kernel/timer.c 파일에서 찾을 수 있다.

[그림 1]의 ⓒ 부분에서는 dev_interrupt 구조체 변수의 expires 변수 값과 현재의 jiffies 변수 값을 비교하여 작거나 같으면 dev_interrupt 구조체 변수의 function 함수 포인터 변수가 가리키는 함수를 수행한다. 이 부분은 timer interrupt를 처리하는 루틴의 bottom_half 부분이며 timer_bh 함수 내에서 run_timer_list 함수를 호출하여 수행한다. timer_bh 함수는 리눅스 커널 소스의 linux/kernel/timer.c 파일에서 찾을 수 있다. [그림 1]의 ⓒ 부분에서는 dev_working 함수의 ⑦ 부분에 의해 실제로는 dev_interrupt_handler 함수가 수행된다.

dev_interrupt_handler 함수를 살펴보기 전에 timer_bh 함수 내의 run_timer_list 함수의 역할을 좀 더 보기로 하자. run_timer_list 함수는 timer_list 구조체 변수로 이루어진 linked list에서 timer_list 구조체 변수를 소비하는 역할을 한다. 구체적으로 timer_list 구조체 변수의 expires 변수 값이 현재 jiffies 변수 값보다 작거나 같을 경우 해당하는 timer_list 구조체 변수를 linked list에서 떼내어, timer_list 구조체 변수의 function 포인터 변수가 가리키는 함수를 수행한다. dev_working 함수의 ⑧ 부분에서 사용한 add_timer 함수는 커널 함수이며 run_timer_list 함수가 소비하는 linked list에 timer_list 구조체 변수를 하나 더해 주는 생산자 역할을 한다. dev_working 함수의 ⑨ 부분에서 사용한 init_timer 함수는 timer_list 구조체 변수를 초기화해주는 커널 함수이다.

그러면 dev_interrupt_handler 함수를 보기로 하자. dev_interrupt_handler 함수는 가상 디바이스의 top_half 루틴과 bottom_half 루틴을 나타낸다. dev_interrupt_handler 함수에서 ⑩ 부분은 논리적으로 다음과 같다.


데이터 큐가 비어 있으면
디바이스를 다 사용했다고 표시하고 나간다


또 dev_interrupt_handler 함수에서 ⑩과 ⑪ 부분은 논리적으로 다음과 같다.


데이터 큐가 비어 있지 않으면
데이터를 하나 꺼내서
디바이스 버퍼에 쓰고 나간다


⑪ 부분에서 full_slot_pos 변수는 데이터를 비울 큐의 위치를 나타낸다.

이상에서 dev_write 함수에서 동기화 문제가 발생할 수 있으며 다음과 같이 해결할 수 있다.





완성된 소스를 다음과 같이 컴파일한 후 insmod 명령어를 이용하여 커널에 devwrite.o 모듈을 끼워 넣는다. 컴파일하는 부분에서 –D__KERNEL__ 옵션은 #define __KERNEL__ 이라는 매크로 문장을 컴파일하고자 하는 파일의 맨 위쪽에 써 넣는 효과와 같으며, __KERNEL__ 매크로는 컴파일하는 소스가 커널의 일부가 될 수 있다는 의미를 가진다. MODULE 매크로는 컴파일하는 소스를 커널에 모듈형태로 동적으로 끼워 넣거나 빼 낼 수 있다는 의미이다. -I/usr/src/linux-2.4/include 옵션은 파일 내에서 참조하는 헤더파일을 찾을 디렉토리를 나타낸다. 일반적으로 PC 상에서 리눅스 커널 소스를 설치할 경우 /usr/src 디렉토리 아래 linux 내지는 linux-2.4 와 같은 디렉토리 아래 놓인다. 모듈 프로그램은 커널의 일부가 되어 동작하며 따라서 그 모듈이 동작할 커널을 컴파일하는 과정에서 참조했던 헤더파일을 참조해야 한다.


# gcc devwrite.c -c -D__KERNEL__ -DMODULE -I/usr/src/linux-2.4/include
# lsmod
# insmod devwrite.o -f
# lsmod
Module Size Used by Tainted: PF
devwrite 3581 0 (unused)


/proc/devices 파일은 커널내의 디바이스 드라이버에 대한 정보를 동적으로 나타낸다. 이 파일을 들여다보면 방금 끼워 넣은 디바이스 드라이버의 주 번호가 253임을 알 수 있다. 주 번호는 바뀔 수도 있으니 주의하기 바란다.


# cat /proc/devices
Character devices:
...
253 devwrite
...
Block devices:


우리가 작성한 디바이스 드라이버를 접근하기 위해 문자 디바이스 파일을 다음과 같이 만든다.


# mknod /dev/devwrite c 253 0
# ls -l /dev/devwrite
crw-r--r-- 1 root root 253, 0 7월 12 00:03 /dev/devwrite


그리고 우리가 작성한 디바이스 드라이버를 사용할 응용 프로그램을 다음과 같이 작성한다.





그리고 다음과 같이 응용 프로그램을 컴파일한 후 응용 프로그램을 수행해 본다. 화면에는 아무 내용도 뜨지 않는다.


# gcc devwrite-app.c -o devwrite-app
# ./devwrite-app


이젠 모듈을 커널에서 빼낸후 /var/log/messages 파일의 맨 뒷부분을 읽어 본다. 각각 insmod 명령어를 수행하는 과정에서 커널내에서 수행한 init_module 함수, 좀 전에 수행한 응용 프로그램을 수행하는 과정에서 커널내에서 수행한 dev_interrupt_handler 함수, rmmod 명령어를 수행하는 과정에서 커널내에서 수행한 cleanup_module 함수에서 찍은 메시지를 볼 수 있다.


# rmmod devwrite
# tail /var/log/messages
...
Jul 12 00:16:44 localhost kernel: Loading devwrite module
Jul 12 00:17:00 localhost kernel: A
Jul 12 00:17:13 localhost kernel: Unloading devwrite module


그러면 위와 같이 동기화 문제를 처리 하지 않을 경우 어떤 문제가 발생할 수 있는지 예를 하나 보기로 하자. 다음 예는 전월호의 [그림 5]와 [그림 6]의 경우에서 보았던 루틴간 경쟁 상태를 발생시킨다. 먼저 dev_write 함수와 dev_interrupt_handler 함수를 각각 다음과 같이 고친다.



dev_write 함수의 ⑫-⑴, ⑫-⑵, ⑫-⑶ 부분은 동기화 문제가 발생할 수 있는 영역을 반복적으로 수행함으로써 dev_interrupt_handler 함수와 충돌이 날 가능성을 높이는 역할을 한다. dev_write 함수의 ⑬-⑴, ⑬-⑵ 사이에 dev_interrupt_handler 함수가 끼어 들 경우 문제가 발생한다. ⑭ 부분은 ⑬-⑴, ⑬-⑵ 사이에 dev_interrupt_handler 함수가 끼어 들 가능성을 높이기 위해 끼워 넣었다. 다음과 같이 테스트해 본다.


# gcc devwrite.c -c -D__KERNEL__ -DMODULE -I/usr/src/linux-2.4/include
# insmod devwrite.o -f
# ./devwrite-app
# tail /var/log/messages -n 600
...
Jul 12 06:19:46 localhost kernel: <--1 ⒜
Jul 12 06:19:46 localhost kernel: 6
Jul 12 06:19:46 localhost kernel: no data ⒞
Jul 12 06:19:46 localhost kernel: <--2 ⒝
Jul 12 06:19:46 localhost kernel: <--1
Jul 12 06:19:46 localhost kernel: <--2
Jul 12 06:19:46 localhost kernel: <--1
Jul 12 06:19:46 localhost kernel: 8 ⒟
Jul 12 06:19:46 localhost kernel: <--2
Jul 12 06:19:46 localhost kernel: <--1
Jul 12 06:19:46 localhost kernel: 7 ⒠
Jul 12 06:19:46 localhost kernel: <--2
...
# rmmod devwrite


테스트를 수행한 결과 ⒜와 ⒝ 사이에 ⒞가 끼어 듦으로써 동기화의 문제가 발생하였다. 그 결과 ⒟와 ⒠에서 8과 7의 데이터 역전 현상이 발생하였으며, 또한 7 데이터에 starvation이 발생하였음을 알 수 있다.

그럼 여기서 발생한 동기화 문제를 해결해 보자. 먼저 dev_write 함수를 다음과 같이 고친다.



다음과 같이 테스트를 수행하다.


# gcc devwrite.c -c -D__KERNEL__ -DMODULE -I/usr/src/linux-2.4/include
# insmod devwrite.o -f
# ./devwrite-app
# tail /var/log/messages -n 600
...
Nov 19 18:41:07 localhost kernel: 0
Nov 19 18:41:07 localhost kernel: <--1
Nov 19 18:41:07 localhost kernel: <--2
Nov 19 18:41:07 localhost kernel: 1
Nov 19 18:41:07 localhost kernel: <--1
Nov 19 18:41:07 localhost kernel: <--2
Nov 19 18:41:07 localhost kernel: 2
Nov 19 18:41:07 localhost kernel: <--1
Nov 19 18:41:07 localhost kernel: <--2
Nov 19 18:41:07 localhost kernel: 3
Nov 19 18:41:07 localhost kernel: <--1
Nov 19 18:41:07 localhost kernel: <--2
Nov 19 18:41:07 localhost kernel: 4
Nov 19 18:41:07 localhost kernel: <--1
Nov 19 18:41:07 localhost kernel: <--2
Nov 19 18:41:07 localhost kernel: 5
Nov 19 18:41:07 localhost kernel: <--1
Nov 19 18:41:07 localhost kernel: <--2
Nov 19 18:41:07 localhost kernel: 6
Nov 19 18:41:07 localhost kernel: <--1
Nov 19 18:41:07 localhost kernel: <--2
Nov 19 18:41:07 localhost kernel: 7
Nov 19 18:41:07 localhost kernel: <--1
Nov 19 18:41:07 localhost kernel: <--2
Nov 19 18:41:07 localhost kernel: 8
Nov 19 18:41:07 localhost kernel: <--1
Nov 19 18:41:07 localhost kernel: <--2
Nov 19 18:41:07 localhost kernel: 9
...


데이터의 역전 현상이나 starvation 없이 순서대로 data가 가상 디바이스에 전달되는걸 볼 수 있다.

이상에서 <디바이스에 쓰기 동작>에 대한 구체적인 작성 예를 보았다. 참고로 모듈 프로그래밍은 일반적으로 루트 사용자의 권한으로 해야 한다. 본 기사에서는 리눅스 커널 2.6 버전의 내용을 위주로 동기화의 문제를 다루고 있지만, 실제 동기화에 대한 테스트는 리눅스 커널 2.4 버전의 파란 리눅스 7.3에서 하였으니 이 점 주의 하기 바란다.
"Kernel" 카테고리의 다른 글
  • SHELL, KERNEL, 응용과의 관계 (0)2007/05/14
  • 리눅스 커널의 이해(5): 디바이스에 쓰기 동작에... (0)2007/05/10
  • 리눅스 커널의 이해(4): Uni-Processor & Multi-Pr... (0)2007/05/10
  • 리눅스 커널의 이해(3): 리눅스 디바이스 작성시... (0)2007/05/10
  • 리눅스 커널의 이해(2): 리눅스 커널의 동작 (0)2007/05/10
2007/05/10 10:29 2007/05/10 10:29
Posted by webdizen
Tags 디바이스, 리눅스 커널
No Trackback No Comment

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

Leave your greetings.

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

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

리눅스 커널의 이해(4): Uni-Processor & Multi-Processor 환경에서의 동기화 문제

저자: 서민우
출처: Embedded World

[ 관련 기사 ]
♠ 리눅스 커널의 이해(1) : 커널의 일반적인 역할과 동작
♠ 리눅스 커널의 이해(2): 리눅스 커널의 동작
♠ 리눅스 커널의 이해(3): 리눅스 디바이스 작성시 동기화 문제

이번 기사부터 3-4회에 걸쳐 리눅스 디바이스 드라이버 작성시 Uni-Processor 또는 Multi-Processor 환경에 따라 발생할 수 있는 동기화 문제의 여러 가지 패턴을 살펴보고 그에 대한 해결책을 알아보기로 하자.

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

[그림 1]은 각각 system call에 의한 리눅스 커널의 동작, hardware interrupt에 의한 리눅스 커널의 동작, nested interrupt에 의한 리눅스 커널의 동작을 나타낸다. 여기서는 시그널을 처리하는 do_signal() 함수를 생략하였다. 일반적으로 리눅스 디바이스 드라이버는 do_signal() 함수와 직접적으로 관련이 없으며, 따라서 여기서는 설명의 편의상 이 부분을 생략하였다.

사용자 삽입 이미지

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


우리는 지난 기사에서 디바이스 드라이버의 주요한 동작을 크게 세가지로 나누었다. 그 세가지는 각각 [디바이스에 쓰기 동작], [동기적으로 디바이스로부터 읽기 동작], [비동기적으로 디바이스로부터 읽기 동작]이다. 이 각각의 동작에 대하여 먼저 Uni-Processor 상에서 발생할 수 있는 동기화 문제와 그에 대한 해결책을, 다음으로 Multi-Processor 상에서 발생할 수 있는 동기화 문제와 그에 대한 해결책을 차례로 살펴보기로 한다.

먼저 위의 세 가지 동작에 대하여 Uni-Processor 상에서 발생할 수 있는 동기화 문제와 그에 대한 해결책을 살펴보자.

[디바이스에 쓰기 동작]에 대한 Uni-Processor 상에서의 동기화 문제와 그에 대한 해결책

지난 기사에서 우리는 [디바이스에 쓰기 동작]과 관련한 커널의 흐름을 보았다. 그 흐름을 좀 더 구체적으로 나타내면 다음과 같다.

▶ 시스템 콜 루틴 내부
i) 디바이스를 사용하고 있지 않으면
디바이스를 사용한다고 표시하고,
데이터를 디바이스 버퍼에 쓰고 나간다
ii) 디바이스를 사용하고 있으면
데이터를 데이터 큐에 넣고 나간다

▶ 하드웨어
디바이스가 데이터를 다 보냈다 → hardware interrupt 발생

▶ top half 루틴 내부
bottom half 요청

▶ bottom half 루틴 내부
i) 데이터 큐가 비어 있으면
디바이스를 다 사용했다고 표시하고 나간다
ii) 데이터 큐가 비어 있지 않으면
데이터를 하나 꺼내서 디바이스 버퍼에 쓰고 나간다

이 흐름은 프로세스를 기준으로 볼 때 논리적으로 두 가지 흐름으로 나눌 수 있으며 각각 다음과 같다.

1) 다른 프로세스가 디바이스를 사용하고 있지 않을 경우
2) 다른 프로세스가 디바이스를 사용하고 있을 경우

각각의 경우를 구체적으로 보자.

디바이스에 쓰기 1

1) 다른 프로세스가 디바이스를 사용하고 있지 않을 경우

▶ 시스템 콜 루틴 내부
i) 디바이스를 사용하고 있지 않으면
디바이스를 사용한다고 표시하고(ⓐ),
데이터를 디바이스 버퍼에 쓰고 나간다

▶ 하드웨어
디바이스가 데이터를 다 보냈다 → hardware interrupt 발생

▶ top half 루틴 내부
bottom half 요청

▶ bottom half 루틴 내부
i) 데이터 큐가 비어 있으면
디바이스를 다 사용했다고 표시하고 나간다

[그림 2]를 통해서 첫번째 흐름을 좀 더 구체적으로 이해해 보자.

사용자 삽입 이미지

[그림 2] 디바이스에 쓰기 1


[그림 2]에서 어떤 프로세스 P1이 시스템 콜을 통해 커널 영역에서 어떤 디바이스를 사용하고자 할 때 다른 프로세스가 그 디바이스를 사용하고 있지 않으면 디바이스를 사용한다고 표시하고, 데이터를 디바이스 버퍼에 쓰고 나간다. 그러면 디바이스는 쓰기 동작을 수행하기 시작한다. 어느 정도의 시간이 지나면 그 디바이스는 쓰기 동작을 완료하고 hardware interrupt를 발생시킨다.

여기서 hardware interrupt는 임의의 프로세스 Pn을 수행하는 중에 발생한다. hardware interrupt가 발생하면 top half 루틴과 bottom half 루틴을 차례로 수행한다. top half 루틴에서는 특별한 일을 하지 않고 bottom half 루틴이 수행되기를 요청한다. 그러면 bottom half 루틴에서는 데이터 큐가 비어 있는지 보고 비어 있으면 디바이스를 다 사용했다고 표시하고 나간다.

여기서 디바이스의 사용은 ① 지점에서 시작해서 ② 지점에서 끝난다. 즉, 시스템 콜 영역에서 시작해서 bottom half 영역에서 끝난다. 일반적으로 이 구간은 CPU를 기준으로 볼 때 시간상으로 무척 길며 얼마나 걸릴지 예측할 수 없다.

사용자 삽입 이미지

[그림 3] 디바이스에 쓰기 2


2) 다른 프로세스가 디바이스를 사용하고 있을 경우

▶ 시스템 콜 루틴 내부
ii) 디바이스를 사용하고 있으면
데이터를 데이터 큐에 넣고 나간다

▶ 하드웨어
디바이스가 데이터를 다 보냈다 → hardware interrupt 발생

▶ top half 루틴 내부
bottom half 요청

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

[그림 3]을 통해서 두 번째 흐름을 좀 더 구체적으로 이해해 보자

[그림 3]에서 어떤 프로세스 Pk가 시스템 콜을 통해 커널 영역에서 어떤 디바이스를 사용하고자 할 때 임의의 프로세스 P1이 그 디바이스를 이미 사용하고 있으면 데이터를 데이터 큐에 넣고 나간다. 디바이스는 이전에 프로세스 P1에 의해 쓰기 동작을 수행하기 시작했다. 어느 정도의 시간이 지나면 그 디바이스는 쓰기 동작을 완료하고 hardware interrupt를 발생시킨다.

이 때 hardware interrupt는 임의의 프로세스 Pm을 수행하는 중에 발생한다. hardware interrupt가 발생하면 top half 루틴과 bottom half 루틴을 차례로 수행한다. top half 루틴에서는 특별한 일을 하지 않고 bottom half 루틴이 수행되기를 요청한다. 그러면 bottom half 루틴에서는 데이터 큐가 비어 있는지 보고 비어 있지 않으면 데이터를 하나 꺼내서 디바이스 버퍼에 쓰고 나간다. 그러면 디바이스는 쓰기 동작을 수행하기 시작한다. 어느 정도의 시간이 지나면 그 디바이스는 쓰기 동작을 완료하고 hardware interrupt를 발생시킨다.

여기서 hardware interrupt는 임의의 프로세스 Pn을 수행하는 중에 발생한다. hardware interrupt가 발생하면 top half 루틴과 bottom half 루틴을 차례로 수행한다. top half 루틴에서는 특별한 일을 하지 않고 bottom half 루틴이 수행되기를 요청한다. 그러면 bottom half 루틴에서는 데이터 큐가 비어 있는지 보고 비어 있으면 디바이스를 다 사용했다고 표시하고 나간다.

여기서 데이터 큐 사용구간 ⒜와 데이터 큐 사용구간 ⒝는 논리적으로 순서를 이루어야 한다. 그렇지 않을 경우에는 문제가 발생하며 이에 대해서는 뒤에 좀 더 구체적으로 다루기로 한다.

지금까지 우리는 [디바이스에 쓰기 동작]과 관련한 커널의 두 가지 논리적인 흐름을 보았다. 이러한 논리적인 흐름이 제대로 지켜지지 않을 경우엔 동기화 문제가 발생할 수 있다.

[디바이스에 쓰기 동작]과 동기화 문제

그러면 지금부터 [디바이스에 쓰기 동작]과 관련한 두 가지 논리적인 흐름에서 생길 수 있는 동기화 문제와 이에 대한 해결책을 생각해 보자.

먼저 지난 기사에서도 말했듯이, 동기화란 논리적으로 흐름이 다른 루틴(예를 들어, 시스템 콜 루틴, top half 루틴, bottom half 루틴)간에 순서를 지키는 일이다. 이러한 루틴간에 순서를 지키지 않는 상황을 루틴간 경쟁 상태라고 한다. 즉, 동기화 문제는 루틴간 경쟁 상태에서 발생한다.

[디바이스에 쓰기 동작]과 관련한 논리적인 흐름에서 생길 수 있는 루틴간 경쟁 상태는 두 가지가 있을 수 있다. 먼저 [시스템 콜 루틴 내부의 i) 항목의 ⓐ 부분]간에 경쟁 상태가 있을 수 있다. 다음으로 [시스템 콜 루틴 내부의 ii) 항목]과 [bottom half 루틴 내부의 i) 항목]간에 경쟁 상태가 있을 수 있다. 좀 더 엄밀히 말하면, [시스템 콜 루틴 내부의 i) 항목의 ⓐ 부분]에 [시스템 콜 루틴 내부의 i) 항목의 ⓐ 부분]이 끼어 드는 상황과, [시스템 콜 루틴 내부의 ii) 항목]에 [bottom half 루틴 내부의 i) 항목]이 끼어 드는 상황이 있을 수 있다.

시스템 콜 루틴간의 경쟁 상태

[그림 4]는 [시스템 콜 루틴 내부의 i) 항목의 ⓐ 부분]간에 경쟁 상태를 나타낸다.

[그림 4]에서 프로세스 P1이 시스템 콜을 통해 커널 영역에서 ⓐ의 앞부분([그림 4]의 ① 부분)을 수행하는 도중에

1) A 지점에서 nested interrupt가 발생하고,

2) B 부분을 포함해 한 번 이상의 프로세스 스케쥴링을 거쳐,
어느 시점에 프로세스 Pn을 수행하고, 프로세스 Pn이 시스템 콜을 통해 커널 영역에서

3) C 지점을 거쳐 ⓐ 부분([그림 4]의 ② 부분)을 수행하고,
이후에 한 번 이상의 프로세스 스케쥴링을 거쳐 어느 순간 프로세스 P1이 h 지점으로 나와(이전에 B 부분의 g 지점으로 들어감) ⓐ의 뒷부분([그림 4]의 ③ 부분)을 수행할 경우 두 프로세스가 같이 디바이스를 사용한다고 표시하는, 그래서 두 프로세스가 디바이스 버퍼를 같이 접근하는, 동기화 문제가 발생한다. 즉, [그림 4]에서 ①과 ③은 논리적으로 연속이어야 하는데 이 사이에 ②가 끼어 드는 상황이 발생한다. 즉, [시스템 콜 루틴 내부의 i) 항목의 ⓐ 부분]간에 경쟁 상태가 발생한다.

사용자 삽입 이미지

[그림 4] 시스템 콜 루틴간의 경쟁 상태


그러면 [시스템 콜 루틴 내부의 i) 항목의 ⓐ 부분]간에 경쟁 상태가 발생하는 이유를 알아보자.

[그림 4]를 보면
1) A 지점에서 nested interrupt를 허용함으로써 동기화 문제가 발생할 가능성이 생기고,
2) B 지점에서 프로세스 스케쥴링을 허용함으로써 동기화 문제가 발생할 가능성이 생기고,
3) C 지점에서 문제가 되는 영역을 접근함으로써 동기화 문제가 구체적으로 발생한다.

일반적으로 동기화 문제는 첫째는 임의의 지점에서 hardware interrupt를 허용함으로써, 둘째는 임의의 지점에서 프로세스 스케쥴링을 수행함으로써 발생한다.

시스템 콜 루틴간의 경쟁 상태에 대한 해결책

그러면 이러한 경쟁 상태를 어떻게 막을지 생각해 보자. 앞에서 우리는 루틴간 경쟁 상태가 발생하는 이유 세 가지를 보았다. 이에 대한 해결책은 각각 다음과 같다.

1) A 지점에서 hardware interrupt를 허용하지 않거나,
2) B 지점에서 프로세스 스케쥴링을 허용하지 않거나,
3) C 지점에서 문제가 되는 영역을 접근하지 못하게 하면 된다.

시스템 콜 루틴간의 경쟁 상태에 대한 해결책

그러면 이러한 경쟁 상태를 어떻게 막을지 생각해 보자. 앞에서 우리는 루틴간 경쟁 상태가 발생하는 이유 세 가지를 보았다. 이에 대한 해결책은 각각 다음과 같다.

1) A 지점에서 hardware interrupt를 허용하지 않거나,
2) B 지점에서 프로세스 스케쥴링을 허용하지 않거나,
3) C 지점에서 문제가 되는 영역을 접근하지 못하게 하면 된다.

좀 더 구체적으로 해결책을 알아보자.

1) 일반적으로 CPU마다 hardware interrupt를 허용하지 않게 하거나 허용하게 하는 명령어를 가지며 이를 이용하여 루틴내의 적당한 구간에서 hardware interrupt를 허용하지 않을 수 있다. 우리는 이 두 명령어를 각각 cli, sti라고 하자. 이 두 명령어를 이용하여 [시스템 콜 루틴 내부의 i) 항목의 ⓐ 부분]을 다음과 같이 처리할 수 있다.


cli
디바이스를 사용하고 있지 않으면
디바이스를 사용한다고 표시하고
sti


여기서 한 가지 주의할 점은 일반적으로 디바이스 버퍼를 접근할 때는 시간상 연속으로 접근할 때 디바이스에 대한 활용도가 높다. 따라서 위의 루틴은 다음과 같이 처리하기로 한다.


cli
디바이스를 사용하고 있지 않으면
디바이스를 사용한다고 표시하고
데이터를 디바이스 버퍼에 쓰고 나간다
sti


2) 일반적으로 프로세스 스케쥴링을 허용하지 않는 것을 schedule lock 또는 preemption lock이라 한다. 리눅스 커널 2.5 버전 이후부터는 preempt_disable(), preempt_enable()이라는 함수를 이용하여 프로세스 스케쥴링을 허용하지 않을 수 있다. preemption lock은 다음에 볼 flag나 lock에 해당하는 변수를 이용하여 논리적으로 독립적인 루틴간에 경쟁 상태를 해결하는 방법이다. 따라서 여기서는 이 방법에 대해 더 이상 구체적으로 다루지 않는다.

3) 논리적으로 flag나 lock에 해당하는 변수를 두어 문제가 되는 영역을 동시에 접근하지 못하게 한다. 예를 들어 문제가 되는 영역에 들어가고자 할 땐 flag를 내리고 들어가고 나올 땐 flag를 올리고 나오는 개념이다. 좀 더 구체적으로 보자. 문제가 되는 영역에 들어가고자 할 땐 다음과 같은 루틴을 수행한다.





즉, 문제가 되는 영역에 들어가고자 할 땐 flag가 올려져 있는지 보고 올려져 있으면 flag를 내리고 들어가고 그렇지 않으면 flag가 올려질 때까지 기다린다.

문제가 되는 영역에서 나올 땐 다음과 같은 루틴을 수행한다.





즉, 문제가 되는 영역에서 나올 땐 flag를 올리고 나온다.

이 두 루틴을 이용하여 [시스템 콜 루틴 내부의 i) 항목의 ⓐ 부분]을 다음과 같이 처리할 수 있다.

사용자 삽입 이미지


그런데 이 루틴의 ⓐ 부분과 ⓑ 부분은 논리적으로 구조가 같다. 따라서 ⓑ 부분에서 발생할 수 있는 동기화 문제가 ⓐ 부분에서도 발생한다. 따라서 이 루틴을 그대로 사용할 경우 문제가 있으며, ⓒ 부분을 다음과 같이 바꾼다.





ⓓ 부분도 다음과 같이 바꾼다.





이 두 루틴을 이용하여 [시스템 콜 루틴 내부의 i) 항목의 ⓐ 부분]을 다음과 같이 처리할 수 있다.

사용자 삽입 이미지



그런데 앞에서도 보았던 것처럼 이 루틴의 ⓐ 부분과 ⓑ 부분은 논리적인 구조가 같다. 따라서 굳이 이와 같은 방법을 사용하지 않고 1)과 같은 방법을 사용하면 된다.

참고로 flag와 같은 속성을 갖는 변수를 세마포어 변수라고 한다. 또 뮤텍스 변수도 이와 같은 속성을 갖는다.

이상에서 [시스템 콜 루틴 내부의 i) 항목의 ⓐ 부분]은 1)과 같이 처리하기로 한다.

시스템 콜 루틴과 bottom half 루틴간의 경쟁 상태

다음은 [시스템 콜 루틴 내부의 ii) 항목]과 [bottom half 루틴 내부의 ii) 항목]간에 경쟁 상태와 이에 대한 해결책을 알아보자.

[그림 5]와 [그림 6]은 [시스템 콜 루틴 내부의 ii) 항목]과 [bottom half 루틴 내부의 i) 항목]간에 경쟁 상태를 나타낸다.

사용자 삽입 이미지

[그림 5] 시스템 콜 루틴과 bottom half 루틴간의 경쟁 상태 1


먼저 [그림 5]에서 프로세스 Pn이 시스템 콜을 통해 커널 영역에서 [시스템 콜 루틴 내부의 ii) 항목]의 앞부분([그림 5]의 ① 부분)을 수행하는 도중에

1) A 지점에서 nested interrupt가 발생하고, B 지점에서 bottom half가 수행되기를 요청하면,

2) C 지점을 거쳐 [bottom half 루틴 내부의 i) 항목]([그림 5]의 ② 부분)을 수행하고 (논리적으로는 [bottom half 루틴 내부의 ii) 항목]을 수행해야 함)

A 지점으로 다시 나와 [시스템 콜 루틴 내부의 ii) 항목]의 뒷부분([그림 5]의 ③ 부분)을 수행한다.

사용자 삽입 이미지

[그림 6] 시스템 콜 루틴과 bottom half 루틴간의 경쟁 상태 2

다음은 [그림 6]에서 프로세스 Pk가 시스템 콜을 통해 커널 영역에서 [시스템 콜 루틴 내부의 ii) 항목]의 앞부분([그림 6]의 ① 부분)을 수행하는 도중에

1) A 지점에서 nested interrupt가 발생하고,
B 부분을 포함해 한 번 이상의 프로세스 스케쥴링을 거쳐, 어느 시점에 프로세스 Pn을 수행하고, 프로세스 Pn의 C 지점에서 hardware interrupt가 발생하여 top half 루틴과 bottom half 루틴을 차례로 수행한다. bottom half 루틴에서는,

2) D 지점을 거쳐 [bottom half 루틴 내부의 i) 항목]을([그림 6]의 ② 부분을) 수행하고 (논리적으로는 [bottom half 루틴 내부의 ii) 항목]을 수행해야 함)
이후에 한 번 이상의 프로세스 스케쥴링을 거쳐 어느 순간 프로세스 Pk가 h 지점으로 나와 (이전에 B 부분의 g 지점으로 들어감) [시스템 콜 루틴 내부의 i) 항목]의 뒷부분([그림 6]의 ③ 부분)을 수행한다.

[그림 5]와 [그림 6]과 같은 경우 디바이스는 사용하지 않으면서 데이터는 데이터 큐에 남아 있는 상황이 발생하며, 일반적으로 이런 상황을 starvation이라 한다. 이와 같은 상황은 데이터 큐 사용구간에서 hardware interrupt에 의한 시스템 콜 루틴과 bottom half 루틴간 경쟁 상태가 발생하여 나타난다. [그림 5]와 [그림 6]에서는 [데이터 큐 사용구간 ⒜]와 [데이터 큐 사용구간 ⒝]간에 경쟁 상태가 발생하였다. 이와 같은 경쟁 상태는 [그림 3]의
[데이터 큐 사용구간 ⒜], [데이터 큐 사용구간 ⒝]와 같은 순서가 되도록 해결해야 한다. 즉, 데이터 큐 사용구간이 겹치지 않도록 한다.

그러면 [시스템 콜 루틴 내부의 ii) 항목]과 [bottom half 루틴 내부의 i) 항목]간에 경쟁 상태가 발생하는 이유를 알아보자.

[그림 5]와 [그림 6]을 보면

1) A 지점에서 nested interrupt를 허용함으로써 동기화 문제가 발생할 가능성이 생기고,
2) 각각 C 지점과 D 지점에서 문제가 되는 영역을 접근함으로써 동기화 문제가 구체적으로 발생한다.

시스템 콜 루틴과 bottom half 루틴간의 경쟁 상태에 대한 해결책

이에 대한 해결책은 이미 앞에서 본 것처럼 각각 다음과 같다.

1) A 지점에서 hardware interrupt를 허용하지 않거나,
2) 각각 C 지점과 D 지점에서 문제가 되는 영역을 접근하지 못하게 하면 된다.

좀 더 구체적인 해결책은 다음과 같다.

1) [시스템 콜 루틴 내부의 ii) 항목]을 다음과 같이 처리하면 된다.


cli
디바이스를 사용하고 있으면
데이터를 데이터 큐에 넣고 나간다
sti


2) 먼저 [시스템 콜 루틴 내부의 ii) 항목]과 [bottom half 루틴 내부의 i), ii) 항목]을 각각 다음과 같이 처리해 본다.







그러나 이렇게 처리할 경우 각각 C 지점과 D 지점에서 데드락이 발생한다. 따라서 C 지점과 D 지점에 다음과 같은 루틴을 사용한다.





그러나 이렇게 처리할 경우 [bottom half 루틴 내부의 ii) 항목]을 [시스템 콜 루틴 내부의 ii) 항목]이후에 수행할 수 있도록 적절한 루틴을 추가해 주어야 하는데 이럴 경우 루틴이 많이 복잡해진다.

[시스템 콜 루틴 내부의 ii) 항목]의 경우 루틴을 수행하는 시간을 예측할 수 있으며, 또한 그 시간이 충분히 짧기 때문에 일반적으로 리눅스 커널에서는 1)과 같은 방법을 사용하여 동기화 문제를 처리한다.

이상에서 [디바이스에 쓰기 동작]에 대하여 Uni-Processor 상에서 발생할 수 있는 동기화 문제와 그에 대한 해결책을 알아보았다. 다음 기사에는 일단 [디바이스에 쓰기 동작]에 대한 구체적인 예를 들여다보기로 하자.
"Kernel" 카테고리의 다른 글
  • SHELL, KERNEL, 응용과의 관계 (0)2007/05/14
  • 리눅스 커널의 이해(5): 디바이스에 쓰기 동작에... (0)2007/05/10
  • 리눅스 커널의 이해(4): Uni-Processor & Multi-Pr... (0)2007/05/10
  • 리눅스 커널의 이해(3): 리눅스 디바이스 작성시... (0)2007/05/10
  • 리눅스 커널의 이해(2): 리눅스 커널의 동작 (0)2007/05/10
2007/05/10 10:26 2007/05/10 10:26
Posted by webdizen
Tags Multi-Processor, Uni-Processor, 리눅스 커널
No Trackback No Comment

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

Leave your greetings.

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

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.

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

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

리눅스 커널의 이해(1) : 커널의 일반적인 역할과 동작

저자: 서민우
출처: Embedded World

본 기사는 리눅스 커널 2.6이 hardware interrupt와 system call을 중심으로 어떻게 설계되었고, 구현 되었는지 살펴본다. 이 과정에서 리눅스 커널 2.6에 새로이 추가된 커널 preemption 기능을 자세히 살펴보기로 한다. 또한 커널의 동기화 문제와 이에 대한 해결책 등을 커널 source 내에서 찾아보기로 하고, 후에 device driver등을 작성할 때 이러한 해결책을 어떻게 적용할 수 있을지도 생각해 본다. 다음으로 리눅스 커널 2.6에 추가된 O(1) scheduler를 소스 수준에서 자세히 살펴 보기로 한다. 또한 task queue의 변형된 형태인 work queue의 사용법을 알아보기로 한다. 마지막으로 리눅스 커널 2.6에서는 어떻게 device driver를 작성해야 할지 구체적인 예를 보기로 한다.

이번 기사에서는 리눅스 커널을 소스 수준에서 구체적으로 들여다 보기 전에 일반적인 커널의 동작을 살펴보고, 이를 바탕으로 리눅스 커널의 전체적인 동작을 살펴보기로 한다.

1. 일반적인 커널의 동작

여기서는 process와 device 사이에서 커널이 수행해야 할 구체적인 역할을 몇 가지 살펴보고, 이를 기본으로 해서 일반적인 커널의 동작을 이해해 보기로 한다.

system call에 의한 커널의 구체적인 동작 1
일반적으로 process는 system call을 통해 커널에게 device로부터 data를 읽기를 요청한다. 그러면 커널은 device로부터 data를 읽기를 요청하고 현재 수행중인 process를 잠시 멈추기 위해 wait queue에 넣는다. 왜냐하면 device로부터 data가 도착해야 그 process를 다시 진행할 수 있기 때문이다(wait, sleep, block과 같은 용어는 이러한 상황에서 쓰인다). 그리고 새로운 process를 적절한 기준에 의해 선택해 수행하기 시작한다. 새로운 process를 선택하고 그 process로 전환하는 과정을 process scheduling이라고 한다. 그 이후에 몇 번의 process scheduling이 더 있을 수 있다.

이 과정을 processor의 관점에서 다시 보자.
processor가 process의 사용자 영역(응용프로그램 영역)을 수행하는 중에 system call 명령을 만나면 커널 영역으로 뛰어 들어간다. 커널 영역에는 device로부터 data를 읽기를 요청하고 현재 수행중인 process를 잠시 멈추기 위해 wait queue에 넣고 새로운 process를 선택해 수행하는 일련의 명령들이 있다(이러한 일련의 명령들을 process 또는 커널이 수행할 작업이라고 하자). 이러한 명령들에 따라 결국 processor는 새로운 process를 선택해 수행하기 시작한다. 그 이후에 몇 번의 process scheduling이 더 있을 수 있으며, processor는 임의의 시간에 임의의 process를 수행하고 있다.

hardware interrupt에 의한 커널의 구체적인 동작
processor가 임의의 process를 수행하는 동안에 device에는 data가 도착한다. device는 data의 도착을 물리적인 신호를 통해서 processor에게 알린다(이를 우리는 hardware interrupt라고 한다). 그러면 processor는 이 신호를 감지하고 커널 영역으로 뛰어 들어간다. 커널 영역에는 device에 도착한 data를 메모리로 읽어 오고, 그 data를 사용할 process에 맞게 적절하게 형태를 바꾸어, data를 기다리는 process에게 전달하고, 그 process를 wait queue에서 꺼내 ready queue로 집어넣은 후, ready queue로 들어간 process의 우선순위가 현재 수행 중이던 process의 우선순위보다 클 경우 process scheduling을 요청한 후 process scheduling을 수행하는 일련의 명령들이 있다. 이러한 명령들에 따라 결국 processor는 device로부터 data를 읽기를 요청한 process를 다시 선택해 수행하기 시작한다. processor는 다시 시작한 process의 커널 영역에서 사용자 영역으로 빠져 나가 사용자 영역을 계속해서 수행한다.

[그림 1]을 보면서 좀 더 구체적으로 이해해 보자. process P1은 사용자 영역의 A 부분에서 system call을 통해 커널 영역으로 들어간다. 커널 영역의 B 부분에서 device로부터 data를 읽기를 요청한 후 현재 수행중인 process를 wait queue에 넣는다. 그리고 C 부분에서 process scheduling을 수행한다. 이 부분을 좀 더 자세히 들여다보면 C 부분에서 시작한 process scheduling은 D 부분에서 끝나지 않고 process P2의 E 부분에서 끝난다. 즉, c 지점으로 들어가서 e 지점으로 나온다.

process scheduling
여기서 process scheduling의 동작을 좀 더 구체적으로 살펴보자. process scheduling은 크게 두 동작으로 나뉜다. 처음 동작은 새로 수행할 process를 선택하는 부분이다. 두번째 동작은 현재 수행하고 있는 process의 상태를 저장한 다음 새로 수행할 process의 상태를 복구하는 것이다. 이 동작을 우리는 문맥 전환이라고 한다. processor는 내부에 여러 개의 register를 가지고 있으며, 이 register를 이용해 process를 수행해 나간다. register는 memory와 같이 data를 저장하는 기능을 하지만, 접근 속도가 memory보다 빠르다. 따라서 비용상 그 개수가 많지는 않다. register는 processor architecture에 따라 R0, R1, ... 또는 EAX, EBX, ... 등의 이름을 가지며, 32 bit RISC processor의 경우 일반적으로 32 bit의 크기를 갖는다. processor는 register와 memory 또는 I/O device내의 register간에
data를 옮겨가면서 procss를 수행해 나간다. 따라서 process를 수행해 나감에 따라 register의 내용은 계속 바뀌게 된다. 문맥 전환 부분을 좀 더 자세히 들여다 보면 processor가 현재 process를 수행해 나가다 어느 순간에 register의 내용을 그대로 메모리에 저장한다. 새로 수행할 process의 경우도 현재 process처럼 이전에 저장한 register의 내용이 메모리에 있으며, 따라서 그 메모리에 저장한 register의 내용을 다시 processor의 register로 복구 시킨다. 그리고 새로운 process를 계속 수행해 나간다.

문맥 전환(context switching)
문맥 전환 부분을 좀 더 구체적으로 이해하기 위해 process scheduling의 동작을 다음과 같이 가정해 보자. 처음 동작에서 새로 수행할 process를 뽑았는데 그 process가 현재 수행하고 있던 process였다. 그러면 두 번째 동작은 다음과 같이 될 것이다. processor가 현재 process의 register의 내용을 메모리에 저장한다. 그리고 방금 전에 메모리에 저장한 register의 내용을 다시 processor의 register로 복구 시킨다. 그리고 현재 process를 계속 수행한다. 이럴 경우 [그림 1]에서 C 부분에서 시작한 process scheduling은 D 부분에서 끝나며, 논리적으로 process scheduling을 수행하지 않은 것과 같다. process scheduling의 본래 목적은 process간의 전환이며 따라서 현재 process와 새로 수행할 process가 있어야 그 본래 기능을 수행할 수 있다. 여기서는 문맥 전환의 동작을 이해하기 위하여 이와 같은 가정을 한 것이다.

사용자 삽입 이미지

[그림 1] system call과 hardware interrupt에 의한 커널의 구체적인 동작


그러면 process scheduling의 본래 기능으로 다시 돌아가 문맥 전환을 생각해 보자.
[그림 1]에서 현재 process를 P1, 새로 수행할 process를 P2라 하자. 그러면 process P1의 C 부분에서 시작한 process scheduling이 논리적으로 D 부분에서 끝나야 하는 것처럼(여기서는 실제로 J 부분에서 끝난다) 이전에 process P2의 F 부분에서 시작한 process scheduling은 논리적으로 E 부분에서 끝나는 것이다. 그러나 시간상으로는 process P1의 C 부분에서 시작한 process scheduling은 process P2의 E 부분에서 끝난다. 즉, c 지점으로 들어가서 e 지점으로 나온다.

이후에 process P2에서 process P3로(f에서 g로), process P3에서 process P4로, 몇 번의 process scheduling이 더 있을 수 있으며(h에서 … i로), 어느 순간 임의의 process Pn이 수행 중일 수 있다. [그림 1]에서 process Pn을 수행하는 중에 G 부분에서, process P1의 B 부분에서 data를 읽기를 요청한 device로부터, hardware interrupt가 들어올 수 있다. 그러면 process Pn은 G 부분에서 커널 영역으로 들어간다. 커널은 H 부분에서 device에 도착한 data를 메모리로 읽어 오고, 그 data를 사용할 process P1에 맞게 적절히 형태를 바꾸어 process P1에게 전달하고, process P1을 wait queue에서 꺼내 ready queue로 넣은 후, 새로이 ready queue로 들어간 process P1의 우선순위가 현재 수행 중인 process Pn의 우선순위보다 클 경우 process scheduling을 요청한다. 그러면 I 부분에서 process scheduling을 수행한다. process Pn의 I 부분에서 시작한 process scheduling은 process P1의 J 부분에서 끝난다. 덧붙이자면, process P1의 C 부분과 J 부분은 시간적으로는 연속이지 않지만 논리적으로는 연속이다.

hardware interrupt에 의한 커널의 일반적인 동작
이제 hardware interrupt에 의해 시작한 커널의 일반적인 동작을 정리해 보자.
[그림 1]에서 process Pn을 수행하는 중에 들어온 hardware interrupt에 의해 시작한 커널의 동작은 다음과 같다.

1. device에 도착한 data를 메모리로 읽어 온다.
2. data를 사용할 process에 맞게 적절하게 형태를 바꾼다.
3. data를 기다리는 process에게 전달하고 process scheduling 요청
4. process scheduling을 수행

여기서 커널의 동작은 크게 세 부분으로 나눌 수 있으며, 그 처음 부분은 다음과 같다.

1. device에 도착한 data를 메모리로 읽어 온다.

이 부분은 hardware interrupt를 처리하는 부분으로써 신속하게 device로부터 data를 읽어냄으로써 빠른 시간 내에 device가 외부로부터 다시 data를 받을 수 있게 한다. 일반적으로 이 부분에서는 또 다른 device로부터 오는 hardware interrupt를 허용하지 않음으로써 신속하게 device로부터 data를 읽어낸다. 리눅스 커널에서는 이 부분을 top half라고 하기도 하고 interrupt handler라고도 한다.

다음으로 두 번째 부분은 다음과 같다.

2. data를 사용할 process에 맞게 적절하게 형태를 바꾼다.
3. data를 기다리는 process에게 전달하고 process scheduling 요청

이 부분은 기본적으로 hardware interrupt를 허용함으로써 응답성을 좋게 한다. 이 과정은 device에서 읽어온 data를 적당하게 처리해 그 data를 기다리는 process에게 전달하고 필요시 process scheduling을 요청한다. 리눅스 커널에서는 이 부분을 bottom half라고도 하고, deferred work라고도 하고, softirq라고도 한다. 덧붙이자면 3번 동작을 리눅스 커널에서는 wake_up이라고 한다.

마지막으로 세 번째 부분은 다음과 같다.

4. process scheduling을 수행

이 부분은 두 번째 부분에서 process scheduling을 요청할 경우 수행한다. 리눅스 커널에서는 이 부분을 schedule이라고 한다.

이상에서 hardware interrupt에 의한 커널의 동작은 [그림 2]와 같다.

사용자 삽입 이미지

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



[그림 1]에서 한 가지 주의할 점은 process Pn의 사용자 영역을 수행하는 중에 들어온 hardware interrupt에 의해 시작한 커널의 동작은 process Pn과 논리적으로 관련이 없다. 따라서 앞에서 설명한 처음 동작과 두 번째 동작을(top half와 bottom half를) 수행하는 중에 현재 process Pn은 논리적으로 멈출 일이 없으며, 따라서 wait queue에 들어갈 일은 없다.

top_half, bottom_half와 system call function간의 통신
마지막으로 한 가지만 더 짚고 넘어가면, [그림 1]에서 process P1의 system call에 의해 시작한 커널과 process Pn의 사용자 영역 수행 중에 발생한 hardware interrupt에 의해 시작한 커널은 각각 논리적으로 독립된 흐름을 가지며 B 부분과 H 부분에서 통신을 한다. 즉, H 부분에서 data를 공급하며, B 부분에서 data를 소비한다. [그림 3]은 [그림 1]의 system call에 의한 커널과 hardware interrupt에 의한 커널간에 data를 주고 받는 상황을 논리적으로 표현한 것이다.

사용자 삽입 이미지

[그림 3] top_half, bottom_half와 system call function간의 통신


system call에 의한 커널의 구체적인 동작 2
[그림 4]를 보면서 다음의 내용을 이해해 보자.
P1, Pn이라 하는 두 process가 있다고 가정하자. process P1는 system call([그림 4]의 A 부분)을 통해 커널에게 process Pn으로부터 data를 받기를 요청할 수 있다. 그러면 커널은 Pn으로부터 P1에게 도착한 data가 있는지 검사한다([그림 4]의 B 부분). P1에게 도착한 data가 없을 경우 커널은 현재 수행중인 process P1을 잠시 멈추기 위해 wait queue에 넣는다([그림 4]의 B 부분). 왜냐하면 process Pn으로부터 data가 도착해야 process P1을 다시 진행할 수 있기 때문이다. 그리고 새로운 process를 선택해([그림 4]의 C 부분) 수행하기 시작한다. 이 동작을 우리는 앞에서 process scheduling이라 했다. 그 이후에 몇 번의 process schduling이 더 있을 수 있다. ([그림 4]에서 process P2에서 process P3로)

사용자 삽입 이미지

[그림 4] system call에 의한 커널의 구체적인 동작


system call에 의한 커널의 구체적인 동작 3

어느 순간 process Pn은 process scheduling에 의해 다시 시작하며([그림 4]의 D 부분) 사용자 영역을 수행하다 system call을 통해([그림 4]의 E 부분) 커널에게 process P1에게 data를 보내기를 요청할 것이다. 그러면 커널은 process Pn으로부터 process P1으로 data를 전달하고([그림 4]의 F 부분), process P1을 wait queue에서 꺼내 ready queue로 넣은 후, ready queue로 새로이 들어간 process P1의 우선순위가 현재 수행 중이던 process Pn의 우선순위보다 클 경우 process scheduling을 요청한 후([그림 4]의 F 부분) process scheduling을 수행한다. process scheduling은 [그림 4]의 G 부분에서 시작해 H 부분에서 끝난다. 즉, process scheduling이 끝나면 process P1이 수행을 다시 시작한다.

system call에 의한 커널의 일반적인 동작
이제 system call에 의해 시작한 커널의 일반적인 동작을 정리해 보자. 먼저 system call은 software interrupt라고도 한다. 주의할 점은 software interrupt는 리눅스 커널내의 bottom half의 또 다른 이름인 softirq와는 관련이 없다.

[그림 1]에서 process P1을 수행하는 중에 들어온 system call에 의해 시작한 커널의 동작은 다음과 같다.

1. device로부터 data를 읽기를 요청한다.
2. 현재 수행중인 process를 wait queue에 넣는다
3. process scheduling을 수행

이 부분은 process의 요청에 의해 커널이 수행하는 영역이며, 상황에 따라 현재 process를 논리적으로 더 이상 진행시킬 수 없는 경우 현재 process를 wait queue에 넣고 process scheduling을 수행할 수 있다. 이 부분은 system call 함수의 일부분이다. 2, 3번 항목은 리눅스 커널의 sleep_on 또는 wait_event와 대응한다.

[그림 4]에서 process P1을 수행하는 중에 들어온 system call에 의해 시작한 커널의 동작은 다음과 같다.

1. process Pn으로부터 도착한 data가 있는지 검사한다.
2. 현재 수행중인 process를 wait queue에 넣는다.
3. process scheduling을 수행

이 부분도 process의 요청에 의해 커널이 수행하는 영역이며, 상황에 따라 현재 process를 논리적으로 더 이상 진행할 수 없는 경우 현재 process를 wait queue에 넣고 process scheduling을 수행한다. 이 부분도 system call 함수의 일부분이다. 여기서도 2, 3번 항목은 리눅스 커널의 sleep_on 또는 wait_event에 대응한다.

[그림 4]에서 process Pn을 수행하는 중에 들어온 system call에 의해 시작한 커널의 동작은 다음과 같다.

1. process P1에게 data를 전달하고 process scheduling 요청
2. process scheduling을 수행

여기서는 커널의 동작을 두 부분으로 나눌 수 있으며, 처음 부분은 다음과 같다.

1. process P1에게 data를 전달하고 process scheduling 요청

이 부분은 process의 요청에 의해 커널이 수행하는 영역이며, system call 함수의 일부분이다. 이 부분은 리눅스 커널의 wake_up에 대응한다.

두 번째 부분은 다음과 같다.

2. process scheduling을 수행

이 부분은 처음 부분에서 process scheduling을 요청할 경우 수행한다.

이상에서 system call에 의한 커널의 동작은 [그림 5]와 같다.

사용자 삽입 이미지

[그림 5] system call에 의한 커널의 일반적인 동작


[그림 5]에서 process scheduling(1)은, 현재 process P의 요청에 따라 커널이 process P와 관련된 작업을 수행하는 도중에 어떤 조건이 맞지 않아, 예를 들어 필요로 하는 data가 없어서, 더 이상 현재 process P의 작업을 진행할 수 없을 경우, 필요로 하는 조건이 맞을 때까지 현재 process P를 wait queue에 넣어 기다리게 하고 나서 수행하는 process scheduling이며, system call function내에서 수행을 한다. 한 가지 기억해야 할 점은 [그림 1]에서 C와 J 부분이 일반적으로 논리적으로는 연속이지만 시간상으로는 연속이 아니듯이 [그림 5]의 process scheduling(1)도 일반적으로 논리적으로는 연속이지만 시간상으로는 연속이 아니다. 후에 process P가 필요로 하는 조건이 맞으면, process P는 논리적인 흐름이 다른 커널(예를 들어, [그림 1]의 H 부분과 같은)에 의해 ready queue로 옮겨지며, 역시 논리적인 흐름이 다른 커널에서 시작한 process scheduling(예를 들어, [그림 1]의 I 부분과 같은)에 의해 [그림 5]의 process scheduling(1)로 나와 system call function의 나머지 부분을 수행한다. system call function 내에서는 이후에도 필요에 따라 process scheduling이 더 있을 수 있다. 이와는 달리 process scheduling(2)는 커널이 process P의 요청에 의해 system call function을 수행하는 도중에 wait queue에서 기다리던 임의의 process를 ready queue로 넣고, 그 ready queue에 넣은 process의 우선 순위가 현재 process P의 우선 순위보다 클 경우(예를 들어 [그림 4]의 F 부분과 같은)에 수행하는 process scheduling이다. 이 경우 현재 process P는 ready queue에 그대로 남아 있다.

system call function과 system call function간의 통신
마지막으로 한 가지만 더 짚고 넘어가면, [그림 4]에서 process P1의 system call에 의해 시작한 커널과 process Pn의 system call에 의해 시작한 커널은 각각 논리적으로 독립된 흐름을 가지며 B 부분과 F 부분에서 통신을 한다. 즉, F 부분에서 data를 공급하며, B 부분에서 data를 소비한다. [그림 6]은 [그림 4]의 system call에 의한 커널간에 data를 주고 받는 상황을 논리적으로 표현한 것이다.

사용자 삽입 이미지

[그림 6] system call function과 system call function간의 통신


process scheduling의 시작과 끝
우리는 [그림 2]와 [그림 5]에서 커널의 일반적인 동작과 process scheduling이 언제 수행되는지 보았다. 아래 [그림 7]에서 process scheduling이 시작되는 부분과 끝나는 부분이 어떻게 연결될 수 있는지 자세히 살펴 보자.
어떤 process의 a 부분에서 시작한 process scheduling은 임의의 다른 process의 b, d, f부분에서 끝날 수 있다. 또 어떤 process의 c 부분에서 시작한 process scheduling도 임의의 다른 process의 b, d, f 부분에서 끝날 수 있다. 마지막으로 어떤 process의 e 부분에서 시작한 process scheduling 역시 임의의 다른 process의 b, d, f 부분에서 끝날 수 있다.

사용자 삽입 이미지

[그림 7] process scheduling의 시작과 끝


지금까지 우리는 커널이 수행해야 할 일반적인 동작이 무엇인지 살펴 보았다. 즉, system call을 통해 시작한 커널의 동작, hardware interrupt에 의해 시작한 커널의 동작을 보았다. 이 과정에서 process와 device 사이에서 커널이 수행해야 할 역할이란 것이 우리가 모르는 그 어떤 것이 아니란 점도 느꼈을 것이다. 의외로 커널의 역할이 지극히 당연한 것들이라고 느꼈을 수도 있다. 또 hardware interrupt에 의해 시작한 커널과 system call에 의해 시작한 커널간의 통신, system call에 의해 시작한 커널과system call에 의해 시작한 커널간의 통신을 보았다. 이 과정에서 논리적으로 서로 독립적인 커널의 동작간에 통신이 어떻게 이루어지는지 구체적으로 알았을 것이다.


http://network.hanbitbook.co.kr/view.php?bi_id=1058
"Kernel" 카테고리의 다른 글
  • 리눅스 커널의 이해(3): 리눅스 디바이스 작성시... (0)2007/05/10
  • 리눅스 커널의 이해(2): 리눅스 커널의 동작 (0)2007/05/10
  • 리눅스 커널의 이해(1) : 커널의 일반적인 역할과... (0)2007/05/10
  • Kprobes를 이용한 커널 디버깅 (0)2007/05/04
  • KernelAnalysis-HOWTO (0)2007/04/30
2007/05/10 09:56 2007/05/10 09:56
Posted by webdizen
Tags hardware interrupt, process scheduling, processor, system call, 리눅스 커널
No Trackback No Comment

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

Leave your greetings.

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

Unix & Linux2007/05/04 17:51

왜 리눅스 커널을 알아야 하는가?

저자: 서민우


일반적으로 리눅스를 이용한 임베디드 시스템의 개발은 다음과 같은 순서로 진행된다.

1) 특정한 용도에 맞추어진 임베디드 하드웨어 설계 및 제작
2) 해당 임베디드 하드웨어에 리눅스 커널 포팅
3) 특정한 용도에 맞는 디바이스 드라이버의 개발
4) 이러한 디바이스를 접근해 적당한 작업을 수행할 응용프로그램 개발

특정한 디바이스를 접근하기 위해 작성한 디바이스 드라이버는 전 단계에서 포팅한 리눅스 커널의 일부가 된다. 따라서 우리는 우리가 작성한 디바이스 드라이버가 리눅스 커널에 어떻게 끼워지는지, 끼워진 후 어떤 흐름에 의해 동작을 하는지 알아야 한다.

일반적으로 리눅스 커널의 포팅은 커널의 내용을 자세히 몰라도 가능하지만 디바이스 드라이버의 개발은 포팅과는 전혀 다른 방향에서 접근해야 한다. 즉, 리눅스 커널의 흐름을 정확히 알아야 디바이스 드라이버의 정확한 개발이 가능하다.


리눅스 커널의 주요한 두 흐름

일반적으로 리눅스 커널로의 진입은 hardware interrupt 와 system call 에 의해서이다. 따라서 hardware interrupt 에 의해 수행되는 routine 과 system call 에 의해 수행되는 routine 의 구조와 흐름의 파악이 커널을 이해하는데 꼭 필요하다. 즉, 우리는 hardware interrupt routine 과 system call routine 을 이해함으로써 리눅스 커널의 대부분을 이해할 수 있다.

hardware interrupt routine

hardware interrupt routine 은 디바이스 컨트롤러 - 예를 들어 이더넷 카드의 제어칩이나 하드디스크의 제어칩 - 의 물리적인 신호에 의해서 시작되는 routine 이다. 그렇다면 이러한 신호는 왜 필요한가?

디바이스 컨트롤러는 이 신호를 이용해 밖에서 오는 데이터의 도착이나 밖으로 나갈 데이터를 모두 보냈음을 알린다.

예를 들어, 이더넷의 경우 밖에서 오는 데이터가 랜선을 통해 컨트롤러 안의 특정한 데이터 저장 영역에 도착하게 된다. 데이터가 정상적으로 도착할 경우 컨트롤러는 물리적인 인터럽트 신호를 통해서 CPU 에 데이터의 도착을 알린다. CPU 의 다음 동작은 칩내에 도착해 있는 데이터를 메모리로 읽어 가는 것이어야 한다. 이러한 CPU 의 동작을 제어하는 루틴이 바로 인터럽트 핸들러의 한 부분이다.

이더넷을 통해 밖으로 데이터를 내보낼 경우도 보자. 먼저 CPU 에 의해 메모리로부터 컨트롤러의 데이터 저장영역으로 데이터가 쓰여져야 하고, 다음으로 컨트롤러는 이 데이터를 랜선을 통해서 밖으로 내 보내야 한다. 컨트롤러가 랜선을 통해 데이터를 내 보내고 있는 동안에는 컨트롤러의 데이터 저장영역은 사용할 수가 없다. 컨트롤러는 데이터 저장영역에 있는 데이터를 밖으로 모두 내보내고 나면 인터럽트를 통해서 CPU 에 데이터 저장영역을 또 쓸 수 있음을 알린다.

지금까지 우리는 인터럽트의 필요성과 그에 따른 CPU 의 동작을 보았다.

이상에서 hardware interrupt routine 의 주요한 내용은 디바이스를 접근해서 도착한 데이터를 읽어오거나 또는 디바이스에 새로운 데이터를 쓰는 것이다.

다음으로 hardware interrupt routine 의 구체적인 동작을 들여다 보자.

새로 도착한 데이터의 경우 일단은 디바이스로부터 메모리로 데이터를 읽어오는 동작이 있어야 하고, 다음으로 메모리로 읽어온 데이터를 적당한 프로세스에게 전달해 주어야 한다. 리눅스에서 앞의 동작을 보통 인터럽트 핸들러의 top half 라 하고 뒤의 동작을 bottom half 라 한다.

굳이 이렇게 인터럽트 핸들러를 두 부분으로 나눈 이유는 다음과 같다. top 부분에서는 신속하게 디바이스를 접근함으로써 디바이스가 빠른 시간 내에 다시 데이터를 받거나 하는 동작을 수행하게 한다. 보통 이 부분에서는 또 다른 인터럽트를 허용하지 않음으로써 이를 가능하게 한다. bottom half 에서는 인터럽트를 열어놓음으로써 또 다른 인터럽트에 대한 응답성을 좋게 한다. 즉, top half 에서는 디바이스에서 데이터를 읽어오는 작업을 하며, bottom half 에서는 읽어온 데이터를 적절히 처리해 적당한 process 에게 전달을 한다.


리눅스에서 hardware interrupt routine 의 일반적인 흐름은 다음과 같다.

사용자 삽입 이미지


그림에서 CPU 가 process 영역 수행 중에 hardware interrupt 가 발생하면 CPU 는 hardware interrupt routine 으로 뛰어 들어간다. interrupt routine 내에서 routine 전후에 process 영역의 문맥을 저장하고 복구한다. do_IRQ 함수 내에 top half 와 bottom half routine 이 모두 포함되며 이 부분에서 인터럽트에 대한 처리를 한다.

timer interrupt 에 의해 interrupt routine 이 수행될 경우 현재 process 의 time slice 가 0 이 될 수 있으며, 이 경우 schedule 함수에서 새로운 process 를 선택해서 그 process 로 작업이 전환될 수도 있다.

do_signal 함수에서는 현재 process 에게 전달된 signal 이 있는지 확인하여 있을 경우에는 signal handler 를 수행한다.


--------------------------------------------------------------------------------

리눅스 커널 2.6의 주요 구조와 응용 세미나


--------------------------------------------------------------------------------


system call routine

다음은 system call routine 을 보자.

system call routine 은 process 에 의해 시작되는 routine 이다. i386 계열의 CPU 의 경우 int(interrupt 의 약자) 란 명령어, arm 의 경우 swi(software interrupt 의 약자) 란 명령어, mips 의 경우 syscall(system call 의 약자) 이란 명령어 등을 사용한다. 즉, process 에 의해 진입하는 커널루틴을 system call routine 내지는 software interrupt routine 이라고 한다.

이러한 system call 은 process 에서 커널을 접근하는 방법인데 그렇다면 왜 이러한 system call 이 필요한가?

process 는 system call 을 통해서 process 영역의 데이터를 커널로 내려 보내기를 요청하거나 또는 커널의 데이터를 process 영역으로 가져오기를 요청한다. 즉, 디바이스가 인터럽트를 통해서 CPU 가 디바이스로부터 데이터를 읽어 가거나 디바이스에 데이터를 쓰기를 요청하듯이 process 는 system call 을 통해서 커널에서 process 의 데이터를 읽어가거나 process 영역으로 데이터를 써 주기를 요청한다. 다음의 예를 보자.

네트워크 통신을 하는 process 의 경우 일반적으로 socket 을 생성해서 그 socket 에 데이터를 쓰거나 읽기를 반복한 후 그 socket 을 닫음으로써 통신을 마친다. socket 에 데이터를 쓰고자 할 경우 process 는 쓰고자 하는 데이터를 만든 후 write 등의 system call 함수를 통해 커널영역으로 데이터를 보낸다. 커널영역에서는 이 데이터를 적당히 가공한 후에 디바이스 컨트롤러로 쓴 후에 process 영역으로 리턴한다. 또 socket 으로부터 데이터를 읽고자 할 경우 process 는 read 등의 system call 함수를 통해 커널로부터 데이터를 읽기를 요청한다. 커널영역에서는 이 프로세스의 socket 을 접근해서 도착한 데이터 - 이 데이터는 hardware interrupt routine 의 bottom half 에 의해서 전달된다 - 가 있는지를 본 후 있으면 socket 으로부터 데이터를 process 영역으로 읽어준 후 process 영역으로 리턴 한다. socket 에 도착한 데이터가 없을 경우 현재 process 의 진행을 임시 중단한 후 스케쥴링을 통해 다른 process 를 수행한다. 흔히 말하는 sleep 이니 wait 니 blocking 이니 하는 용어는 이러한 상황에서 쓰인다.

물론 process 는 system call 을 통해서 이외에도 다른 여러 가지 작업을 커널에 요청한다.


System call routine 의 일반적인 흐름은 다음과 같다

사용자 삽입 이미지


현재 process 의 swi 등의 명령어에 의해 수행되는 system call routine 은 다른 부분은 hardware interrupt routine 과 같으나 sys_func 함수 호출하는 부분이 다르다. 이 부분에서 커널영역의 여러 데이터를 건드릴 수 있다. 중간에 두 개의 줄 사이에서 interrupt 를 허용한다. 따라서 이 부분에서 interrupt routine 이 겹칠 수 있다. 이 부분에 대해서는 추후에 좀 더 다루고자 한다.


두 루틴간의 상호 작용

다음 그림을 보자.

사용자 삽입 이미지


이 그림의 왼쪽은 hardware interrupt routine 이며 오른쪽은 software interrupt routine 이다. 이처럼 두 루틴은 일반적으로 중간에 공유버퍼를 두고 데이터를 주고 받는다. 그리고 우리가 작성할 디바이스 드라이버는 이 두 루틴에 모두 포함된다.


마무리

이상에서 우리는 hardware interrupt routine 과 system call routine 의 일반적인 동작을 보았다. 이 두 routine 에는 디바이스를 접근하는 routine 이 포함되며, 이러한 디바이스를 접근하는 routine 을 우리는 device driver 라고 한다. 우리가 작성하는 device driver 는 hardware interrupt routine 과 system call routine 에 모두 포함된다. 즉, 커널의 일부가 되어 해당 디바이스를 접근한다. 따라서 이 두 routine 의 흐름을 알지 않고서, 즉 커널의 흐름을 알지 않고서 거기에 끼워 넣어질 device driver 를 제대로 작성하기란 불가능하다.
"Unix & Linux" 카테고리의 다른 글
  • 리눅스 명령어 (6)2007/07/11
  • 왜 리눅스 커널을 알아야 하는가? (0)2007/05/04
  • [처음부터 다시 배우는 리눅스] ③ vim 편집기 활용법 (0)2007/05/04
  • [처음부터 다시 배우는 리눅스] ② 고급 명령과 시... (0)2007/05/04
  • [처음부터 다시 배우는 리눅스] ① 설치와 기본 명... (0)2007/05/04
2007/05/04 17:51 2007/05/04 17:51
Posted by webdizen
Tags 리눅스 커널
No Trackback No Comment

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

Leave your greetings.

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

Programming/UNIX/Linux C2006/12/27 14:53

리눅스 커널 코딩 스타일

고수닷넷 - 데미소다오렌지님

리눅스 커널 코딩 스타일

이 문서는 리눅스 커널에서 주로 사용되는 코딩 스타일을 설명한다. 코딩 스타일은 매우 개인적이므로 나의 견해를 다른 사람에게 강요하고 싶진않다. 하지만 이것은 내가 성취하기 위해서 유지해야만 하는 것들이며 나는 다른 어떤 것보다도 이 스타일을 좋아한다. 최소한 여기서 지적하는 부분에 대해서 한번씩 생각해 보길 바란다.

우선, 난 GNU 코딩 표준 문서를 한 부 출력하기를 제안하고 싶다. 그리곤 읽지않고 태워버리자. 그건 매우 상징적인 제스쳐가 될 것이다.

하여튼, 한번 시작해 보도록 하자.

챕터 1: 인덴트

탭은 8 글자, 마찬가지로 인덴트도 8글자로 한다. 인덴트를 4(또는 2)글자의 깊이로 바꾸려는 이단자들의 움직임이 있다. 그것은 파이(PI)값을 3으로 선언하는 것과 마찬가지의 행동이다.

근거: 인덴트 뒤에 숨어있는 전체적인 아이디어는 블록 컨트롤이 어디서 시작하고 끝나는지 명확하게 선언하는 것이다. 특히 당신이 모니터를 20시간동안 뚤어져라 쳐다봐야 하는 상황에서는 그 점을 좀 더 쉽게 발견할 수 있다. 만약 넓은 인덴트를 사용했다면 그것이 어떻게 동작하는지 쉽게 볼 수 있기 때문이다.

지금, 몇몇 사람들은 8글자 인덴트를 가지고 있는 것은 코드를 오른쪽으로 너무 멀리 이동시키며, 그것은 80 글자 터미널 스크린에서 더욱 읽기 힘들다고 주장한다. 거기에 대한 답변은 3 레벨 이상의 인덴트가 필요가 없다는 것이다. 만약 그들이 3레벨 이상의 인덴트를 필요로 한다면 그들의 코드는 어쨌든 꼬이는 것은 마찬가지이기 때문에 반드시 프로그램을 고쳐야 할 것이다.

짧게 말해서, 8글자 인덴트는 코드를 읽기 쉽게 만들어 주며 너무 깊게 함수를 중첩시키는 것을 경고해주는 추가적인 잇점이 있다. 그 경고를 무시해서는 안된다.

챕터 2: 괄호 위치

C 스타일과 관련되서 항상 튀어나오는 다른 이슈는 괄호를 어디다 위치시키냐 하는 것이다. 인덴트 사이즈와 다르게, 다른 것을 배제하고 한 가지 배치 전략을 선택하는 것은 아주 조금의 기술적인 이유밖에 없다. 그러나 좋아라 하는 방법은 열기 괄호는 라인 끝에 위치시키고, 닫기 괄호는 라인의 처음에 위치시키는 것이다. 이러한 방법은 커닝헌과 리치와 같은 선지자들이 우리에게 보여준 것이기도 하다. 아래와 같이 될 것 이다.

       if (x is true) { 
              we do y 
       } 

그러나, 거기에는 함수라는 한가지 특별한 경우가 있다. : 그것들은 다음과 같이 열기 괄호를 다음 줄의 시작에 위치시킨다.

       int function(int x) 
       { 
              body of function 
       } 

전세계에 분포한 이단자들은 이것은 모순이다... 음 불완전한다...,라는 주장을 펼친다. 그러나 바르게 생각하는 모든 사람은 (a) K&R은 올바르며 (b) K&R도 옳다는 것을 알고 있다. 게다가, 함수는 어쨌든 특별하다 (C에서 함수는 중첩이 불가능하다).

닫기 괄호는 그것 자체만 있는 줄로 비어 있어야 한다는 점을 알아둬라. 물론 연속적으로 같은 구문이 따라오는 경우는 제외한다. 예를들면 do 구문에 포함된 "while" 이나 if 구문에 포함된 "else"같은 것들이다. 그것들은 다음과 같이 쓴다.

       do { 
              body of do-loop 
       } while (condition); 
또는
       if (x == y) { 
              .. 
       } else if (x > y) { 
              ... 
       } else { 
              .... 
       } 

근거: K&R.

또한, 이렇게 괄호를 배치시키는 것이 가독성에 대한 어떠한 손해도 없이 비어있는(또는 거의 빈) 라인의 수를 최소화 한다는 것을 알아야 한다. 그렇게 함으로써, 낮은 해상도의 터미널에서도(80x25라인 터미널이 여기 있다고 생각해봐라), 코멘트를 달 수 있는 빈 라인들을 더 얻을 수 있을 것이다.

챕터 3: 네이밍

C는 엄격한 언어며, 네이밍에도 그러한 것이 적용된다. Modula-2나 Pascal 프로그래머들과 달리, C 프로그래머들은 ThisVariablesATemporaryCounter와 같은 귀여운 이름을 사용하지 않는다. C 프로그래머는 아마도 그 변수 이름을 "tmp"로 지을 것이다. 그러는 편이 훨씬 쓰기 쉽고, 그다지 이해하기 어렵지도 않기 때문이다.

그러나, 대소문자가 섞인 이름이 눈살을 찌푸리게 하지만 , 전역 변수는 반드시 설명적인 이름으로 지어야 한다. 전역 함수를 "foo"라고 부르는 것은 잘못된 것이다.

전역 변수는(실제로 그것들을 필요할때만 사용해야 한다) 전역 함수가 그랬듯이 설명적인 이름을 가질 필요가 있다. 활성화된 사용자 수를 세는 함수가 있다고 가정해 보자. 아마도 "count_active_users()"내지는 그와 비슷한 이름을 지을 것이다. 그걸 "cntusr()"이라고 불러서는 안된다.

함수 타입을 이름에 인코딩 하는 것은(소위 헝가리언 표기법이라 불린다) 완전히 쓸모없는 짓이다. - 컴파일러는 어쨌든 타입을 알고 있으며 그것들을 체크할 수 있다. 그리고 그것은 단지 프로그래머를 혼란시키기만 한다. 마이크로소프트가 버그 투성이의 프로그램을 만드는 것에 대한 의문의 여지가 없는 것이다(역주: MS의 수석 개발자 찰스 시모니가 헝가리언 표기법을 창안했다).

지역 변수 이름은 반드시 짧아야 한다는 것이 포인트다. 정수 루프 카운터에 사용되는 변수는 "i"로 불리는 것으로 족하다. 거기에 오해의 소지가없는한, 그것을 "loop_counter" 라고 부르는 것은 생산적이지 않다. 마찬가지로 "tmp"는 임시 값을 가지고 있는데 사용하는 어떤 타입의 변수도 될 수 있다.

만약 당신 지역 변수 이름을 섞어 쓸것 같아서 걱정된다면, 당신은 다른 문제도 가지고 있을 것이다. 그것은 함수-성장-호르몬-불균형 신드롬이라고 불리는 것이다. 다음 챕터를 보면 알게 될 것이다.

챕터 4: 함수

함수는 한가지 일만하는 간단하고 규칙적인 것이어야 한다. 그것들은 스크린 크기의 두배 정도의 텍스트에 들어가야하며(우리 모두가 알고 있듯이 ISO/ANSI 스크린 사이즈는 80x24다.) 한가지일을 처리하며 그것을 잘 해야한다.

함수의 최대 길이는 해당 함수의 인덴트 레벨과 복잡도에 역으로 비례한다. 따라서, 컨셉적으로 간단한 함수들에 대해서는 길게 작성하는 것도 괜찮다. 주로 그런것들은 많은 수의 작은 것들을 많은 다른 경우에 수행해야 하는때에 사용하는, 단지 길기만 한(그러나 간단한) case 구문으로 구성된것들이다.

그러나, 복잡한 함수라서 고삐리들 조차도 그 함수가 무엇을 하는 것인지 정확하게 이해하지 못한다고 생각한다면, 반드시 세밀하게 두페이지 분량의 제한을 지킬 필요가 있다. 그럴때에는 대신 설명적인 이름을 지닌 헬퍼 함수들을 사용하면 된다 (만약 그것이 성능과 매우 밀접한 연관이 있다면 inline함수를 사용할 수 있다. 그리고 그것이 네가 할 수 있는 행동중에 가장 괜찮은 것이다).

함수의 다른 측정 기준의 하나는 지역 변수의 갯수이다. 그것들은 5-10개를 넘지 않아야 한다. 그렇지 않으면 뭔가 잘못되고 있는 것이다. 함수에 대해서 다시 생각해 보고, 그리고 그것을 더 작은 조각으로 분리하도록 해야 한다. 사람의 뇌가 일반적으로 쉽게 기억할 수 있는 것은 7가지 다른 것들에 불과하다. 그것보다 많다면 혼란을 야기시킬 것이다. 네가 똑똑하다고 생각한다면 지금으로부터 2주전에 작업한 것이 무엇인지 이해해 보길 바란다.

챕터 5: 코멘트

코멘트는 좋다. 그러나 과도한 코멘트에는 또한 위험이 도사리고 있다. 절대로 어떻게 코드가 동작하는지를 코멘트로 설명하려고 시도하지 마라. 코드를 그것이 하는일이 분명해 지도록 쓰는것이 좋다, 그리고 잘못 씌여진 코드를 설명하는 것은 시간 낭비다.

일반적으로, 코멘트는 코드가 무엇을 하는지를 말해야한다. 어떻게 하는지를 말하는 것이 아니다. 또한 코멘트를 함수 내부에 두는 것을 피해야한다. 만약 함수가 너무 복잡해서 그것의 일부분을 분리해서 코멘트로 남겨야 할 필요가 있다면, 챕터 4로 돌아가 보길 바란다. 특별한 꽁수에(이상한것) 경고나 알림을 하는 작은 코멘트를 만들수는 있다. 그러나 과도한것은 피해야한다. 대신, 코멘트를 함수 앞에 두도록 한다. 사람들에게 그것이 무엇을 하는지, 왜 그것을 하는지 말해주는 것이다.

챕터 6: 모든걸 망쳐 버렸을 때

괜찮다. 우린 모든것을 할 수 있다. 오래된 유닉스 사용자들은 "GNU emacs"가 C 소스를 자동으로 포맷팅할 수 있다고 말한다. 맞는 말이다. emacs는 그걸 해준다. 그러나 디폴트 옵션을 사용한다면 우리가 원했던 것을 얻을 수 없을 것이다 (사실, 그것은 랜덤 타이핑 보다 못하다 - 무한대의 원숭이가 GNU emacs에서 타이핑 해봤자 좋은 프로그램이 만들어질리 없다).

이쯤되면 아마 GNU emacs를 사용하지 않는다는 사람도 있을 것이고 스캐너 옵션을 변경하려는 사람도 있을 것이다. 후자쪽을 선택했다면, .emacs 파일에 아래와 같은 줄을 추가하면 된다.

(defun linux-c-mode () 
  "C mode with adjusted defaults for use with the Linux kernel." 
  (interactive) 
  (c-mode) 
  (c-set-style "K&R") 
  (setq c-basic-offset 8)) 

이것은 M-x linux-c-mode 커맨드를 정의하는 것이다. 파일의 처음 시작 두줄에 -*-linux-c-*- 이 문장을 놓게되면 이 모드는 자동적으로 실행된다. 이것을 추가하고 싶을 것이다.

(setq auto-mode-alist (cons '("/usr/src/linux.*/.*\\.[ch]$" . linux-c-mode) 
                       auto-mode-alist)) 

위의 내용을 .emacs 파일에 추가하면, /usr/src/lunux 아래의 소스를 편집할때 자동으로 linux-c-mode가 켜지게 된다.

emacs를 사용해서 코드를 포맷팅하는데 실패했다면 좌절할 필요없다. "indent"를 사용하면 된다.

GNU indent는 GNU emacs처럼 쓸모없는 설정을 가지고 있다. 이것이 바로 몇줄의 커맨드라인 옵션을 주어야 하는 이유다. 그러나, 나쁘진 않다. 왜냐하면 GNU 인덴트도 K&R의 권위에 대해서는 인식하고 있기 때문이다 (GNU 사람들이 악마는 아니다. 그들은 단지 이 문제를 심하게 잘못 인도하고 있을 뿐이다), 따라서 인덴트 옵션으로 단지 "-kr -i8" ("K&R", 8 글자 인덴트를 의미한다)만을 주면 된다.

"indent"는 많은 수의 옵션을 가지고 있다. 특히 . 코멘트를 다시 포맷팅해야 할때에는 매뉴얼 페이지를 한번 쯤 읽어보는 것이 도움이 될 것이다. 그러난 기억해 두어야 할 것은 "indent"는 잘못된 프로그램을 고쳐주진 않는다는 점이다.

챕터 7: 환경 설정 파일

환경 설정 옵션들(arch/xxx/config.in과 모든 Config.in 파일들)을 위해서는 다른 인덴트가 사용된다.

코드에서는 인덴트 레벨 3이 사용되었다. 반면에 환경 옵션을 위한 텍스트에서는 의존 관계를 구분하기 위해서 인덴트 레벨 2를 사용한다. 후자의 경우는 단지 bool/tristate(삼상) 옵션에만 적용된다. 다른 옵션에는 단지 상식을 적용시키면 딘다. 예를 살펴보자.

if [ "$CONFIG_EXPERIMENTAL" = "y" ]; then 
   tristate 'Apply nitroglycerine inside the keyboard (DANGEROUS)' CONFIG_BOOM 
   if [ "$CONFIG_BOOM" != "n" ]; then 
      bool '  Output nice messages when you explode' CONFIG_CHEER 
   fi 
fi 

일반적으로, CONFIG_EXPERIMENTAL은 안정적이지 않다고 생각되는 모든 옵션을 둘러싸고 있어야 한다. 데이터를 파괴하는 것으로 알려진(파일 시스템 쓰기 기능 지원같은) 옵션들은 (DANGEROUS)로 표기되어야 한다. 다른 실험적인 옵션은 (EXPERIMENTAL)로 표기된다.

챕터 8: 자료 구조

다른 쓰레드에서 접근할 수 있는 자료 구조들은 반드시 레퍼런스 카운트를 가져야 한다. 커널내부에는 카비지 컬렉션은 존재하지 않는다 (그리고 커널 가비지 컬렉션은 느리고 비효율적이다). 그것은 사용하는 것마다 무조건 레퍼런스 카운트를 가져야 한다는 것을 의미한다.

레퍼런스 카운트는 락을 피할수 있고, 여러 사용자들이 자료 구조에 병렬적으로 접근하는 것을 허럭한다는 것을 의미한다. - 그리고 슬립이나 잠시동안 다른 일을 처리하느라 자료 구조들이 갑자기 사라지는 현상에 대해서 걱정할 필요는 없다.

락이 레퍼런스 카운트의 대안이 될 수 없음을 알아야 한다. 락은 자료 구조들이 일관성을 유지하기 위해서 사용한다. 반면에 레퍼런스 카운트는 메모리 관리 테크닉이다. 일반적으로 둘다 필요하고 그것들이 서로 혼란스럽지 않다.

많은 자료 구조들은 다른 "classes"의 유저들이 있을때, 두가지 단계의 레퍼런스 카운트를 가지고 있다. subclass 카운트는 subclass 사용자들의 갯수를 헤아린다. 그리고 전역 카운트는 subclass 카운트가 0이 되었을때 단지 한번만 감소 시킨다.

이러한 종류의 다중 레퍼런스 카운팅(multi-reference-counting)은 메모리 관리("struct mm_struct": mm_users와 mm_count)에서 발견된다. 또한 파일 시스템("struct super_block": s_count 와 s_active) 코드에서도 볼 수 있다.

분명하게 기억해야 할 것은 다른 쓰레드에서 참조하는 자료 구조에 레퍼런스 카운트가 없다면, 거의 대부분의 경우 거기에는 확실한 버그가 있다.

"UNIX/Linux C" 카테고리의 다른 글
  • 두 변수의 값을 바꾸는 환상의 매크로 (0)2007/02/22
  • 내장 매크로를 사용한 버전 자동화 기법 (0)2006/12/27
  • 리눅스 커널 코딩 스타일 (0)2006/12/27
  • 표준 입력 버퍼의 진상. (0)2006/12/27
  • [PDF] Numerical Recipes in C (0)2006/12/17
2006/12/27 14:53 2006/12/27 14:53
Posted by webdizen
Tags Linux Kernel, 리눅스 커널
No Trackback No Comment

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

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

  • UDP
  • SCCM
  • 미래
  • Foundstone
  • 악성 소프트웨어
  • 인덱싱 기법
  • Tip
  • 플러그인
  • 비즈니스 모델
  • Hyper-V
  • MIB
  • 휘닉스 파크
  • Events
  • 조니워커 골드
  • 청년 리더
  • 연적지
  • lynx
  • 웹 로그
  • 패턴
  • MITS 4650

Recent Articles

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

Recent Comments

  • 관리자만 볼 수 있는 댓글입....
    비밀방문자 03/12
  • 상대방의 이야기를 열심히 경....
    DoNuts 03/03
  • Lots of students know techn....
    Bobbi35Shannon 02/25
  • 좋은글 잘 보고 갑니다..
    Und_hacker 01/08
  • 재밌네요~ 첫번째꺼는 요즘....
    Hybrid 2009

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.