영문 윈도우즈...MUI

<출처 : 2001.10.15 | HiTEL Windows 동호회(k2win4@hitel.net)>

외국에 유학한 유학생이나 외국계 기업에 다니는 회사원, 외국에 수출하는 소프트웨어를 개발하는 프로그래머라는 세 계층은 척 보기엔 큰 공통점이 없어 보인다. 외국자 들어가는 것 빼고는. 하지만 이들은 공통적인 문제점을 하나 안고 있다. 사람은 한국인인데 컴퓨터의 운영체제는 영문 운영체제를 써야 하는 일이 생긴다는 것이다. 아무리 영어를 잘 아는 사람이라 할지라도 영문 운영체제는 한글 운영체제보다 작업 효율성은 떨어질 수 밖에 없다. 결국 불편을 감수하면서 영문 운영체제를 사용해야 하거나 한글 운영체제와 다중 부트 형식으로 사용할 수 밖에 없다.(VMWare와 같은 방식은 논외로 하자. 보통 사람은 잘 사용하지도 않는 방식이므로......)

상식적으로 생각해보면 두개의 운영체제를 필요에 따라 서로 바꾸어가며 부팅하는 것이 가장 좋아 보인다. 그러나 이 방법도 문제가 없는것은 아니다. 운영체제를 두개 설치하는 시간도 시간이지만 공간 낭비도 무시할 수 없다. 같은 소프트웨어를 따로 설치해주어야 하므로 이래저래 불편한 것은 아니다.(머리를 쓰면 이것도 최소화 할 수 있지만 크게 줄진 않는다.) 어차피 한 사람이 운영체제를 2개 사용하는 것인데 이것은 너무 커다란 불편이요 낭비이다.(배부른 소리라고 말하는 것은 상관 없다. 다음 글을 다 읽은 후에.)


앞에서 낭비, 낭비라고 외치는데 그러면 방법이 있는가? 방법이 있으니까 이런 소리라도 할 수 있는 것이다. 이러한 불편함을 없앨 수 있는 획기적인 방법을 Microsoft에서는 제공하고 있다. 불가능을 가능케 하는 비법, 그것이 바로 MUI 팩이다.




 MUI란 무엇인가?


MUI는 Multilingual User Interface의 약자이다. 한글로 굳이 쓰자면 다국어 사용자 인터페이스정도로 번역될 것인데 이것은 하나의 운영체제에 언어와 인터페이스만 바꾸어 주는 기능을 말한다.


쉽게 말해보자. 일반적으로 영문 운영체제라 한다면 시작 메뉴부터 안에 포함된 기본 소프트웨어, 글꼴, 키보드 및 마우스, 도움말이 모두 영문으로 표시되며 한글이나 일본어, 중국어, 아랍어는 정상적으로 표시되지 않는 운영체제를 말할 것이다. Windows 2000이 되면서 적어도 영문 운영체제에서 다른 언어를 보고 입/출력하는 것은 어느정도 융통성이 생기게 되었지만 여전히 기능과 애플리케이션은 영문판이다. 이것만으로도 사용상 불편함을 느끼기엔 매우 충분하다. MUI 패키지는 이 영문판 운영체제를 문자 입/출력 시스템부터 메뉴, 애플리케이션, 도움말까지 모두 한글화 또는 일본어, 중국어, 아랍어화 시킬 수 있다. 거의 완벽한 한글, 일본어, 중국어, 아랍어 Windows로 탈바꿈 할 수 있는 것이다.


 MUI 패키지에 대한 몇가지 의문점


영문판 운영체제를 쉽게 한글판, 일본어판, 중국어판 운영체제로 바꿀 수 있다는 점은 귀가 솔깃한 이야기가 될 것이다. 하지만 무조건 기뻐하기 전에 몇가지 의문점이 들 것이다. 일반적으로 MUI 패키지에 대해 많은 사용자가 문의하는 점에 대해 간략히 정리하여 보았다.


문 : MUI 패키지를 한글 Windows에 설치하면 영문, 일본어, 중국어 Windows가 됩니까?


답 : 그렇지 않습니다. MUI 패키지는 영문 운영체제에만 설치할 수 있습니다. 기타 언어 운영체제에는 설치할 수 없습니다.


문 : 저는 Windows XP Home Edition 영문판을 이용합니다. 여기에 Windows 2000 MUI를 설치할 수 있는지요?


답 : 설치할 수 없습니다. MUI는 Windows의 버전에 따라 내용물이 전혀 달라지므로 이전 버전 또는 이후 버전의 MUI는 사용할 수 없습니다. 또한 Windows XP의 경우 Home Edition은 MUI 패키지를 설치할 수 없습니다.


문 : MUI 패키지는 어디에서 구입할 수 있습니다.


답 : 시중에서는 MUI 패키지를 구입하실 수 없습니다. 원칙적으로 MUI 패키지는 이러한 환경에 있는 기업에만 특별히 공급되며 시중에서 따로 판매되진 않습니다. MUI의 구입은 각 국가의 Microsoft 지사에 문의하여 주십시오.


문 : MUI 패키지는 어느정도의 디스크 공간을 사용합니까?


답 : 얼마나 많은 언어의 MUI를 설치하는가에 따라 다릅니다만 최소 100MB에서 500MB까지의 추가적인 디스크 공간을 요구합니다. 용량이 큰 것처럼 보이지만 실제 2개 이상의 운영체제를 설치하는 용량에 비해서 훨씬 작은 크기입니다.


 MUI의 설치


Windows XP MUI를 설치하기 위해서는 일단 영문판 Windows XP Professional과 추가적인 디스크 공간이 필요하다. 준비가 되었다면  Windows XP MUI 패키지 CD를 넣는다.



 


 MUI 패키지의 CD를 넣으면 설치 화면이 시작된다. 이 화면은 사용자 계약서로서 반드시 한번은 읽어 보아야 할 정보를 담는다.(특히 MS의 사용자 계약서는 법률과 법률로 얽힌 법률의 함정이라 볼 수 있을 정도로 애매모호하고 나중에 문제가 될 수 있는 부분이 많다.) 철저히 읽고 동의할 경우 동의하는 항목에 체크를 하고 다음으로 넘어간다.



 


 설치할 언어 팩을 고르고 기본 언어를 설정한다. 한국인이라면 당연히 Korean에 체크를 할 것이고 필요하다면 일본어나 중국어(간체/번체), 독일어 등에도 체크를 한다. 또한 밑의 Default User Settings에서 기본 언어를 설정한다. 한글 입/출력을 원한다면 Korean으로 설정하고 OK를 누른다.



 


 일단 Windows XP 원본에서 각 언어에 맞는 입/출력 시스템을 복사/설치한다. Windows XP 원본 CD를 준비한다. 이 작업은 몇분정도 소요된다.



 이제 Windows XP MUI 패키지에서 UI 파일을 설치한다. 이 작업은 몇분에서 몇십분까지 걸릴 수 있으나 일반적인 시스템이라면 몇분내로 복사가 완료된다.




 복사 작업이 완료되면 복사가 완료되었다는 창이 표시된다. OK를 누르면 재부팅을 요구하므로 재부팅을 하도록 한다.


 언어를 변경하자


설치하고 재부팅을 해도 영문 Windows 상태에서 변함은 없다. MUI 패키지는 과연 가짜인가? 그렇지는 않다. 지금부터 UI를 변경하는 방법에 대해 간략히 살펴보기로 하자.



 제어판(Control Panel)에 가면 국가별 설정 항목으로 들어간다.



 이 항목에는 3가지 탭이 있는데 일단 Regional Options 탭에 있는 Standards and Formats와 Location 항목이 정상적으로 한국(또는 설정을 변경하고자 하는 언어를 사용하는 국가)으로 되어 있는 지 확인한다. 이 부분은 UI 변경과 관계는 없으나 단위 표시 및 정보 제공에 중요한 역할을 한다.



 Languages 탭으로 이동하면 맨 하단에 Language used in menu and dialogs 항목이 나타난다. 여기에는 추가한 MUI의 언어가 표시되는데 사용자가 원하는 언어로 변경한다. 변경 후 OK 또는 Apply를 눌러 적용한다.



 현재 사용자의 로그 오프를 해야 적용된다는 메시지가 표시된다. 재부팅 또는 사용자 로그 오프를 해준다. 이렇게 하면 설정된 언어로 모든 UI가 변경된다. 변경되기 전 UI와 변경된 UI의 차이를 아래에서 볼 수 있다.



 

 MUI의 장점과 단점


 장점


 요구되는 디스크 공간을 최소화 할 수 있다. 일반적으로 두가지 언어의 Windows를 독자적으로 설치하려면 최소한 300MB, Windows NT 계열 운영체제는 이보다 훨씬 많은 공간을 요구한다. 하지만 MUI를 설치할 경우 100MB 내외의 공간 점유만을 하므로 적은 디스크 공간에서도 최대한의 효율성을 발휘한다.


 언어의 전환이 매우 빠르다. 다중 부트의 경우 최소한 한번 재부팅을 하여야 하고 2개의 디스크를 이용할 경우 경우에 따라 BIOS에서 부팅 순서를 변경해야 하는 번거로움이 생기지만 MUI를 사용할 경우 언어 전환 후 로그 오프만으로도 언어를 쉽게 변경할 수 있다.


 일반적으로 타 언어 OS에서는 한글의 표시와 입/출력에 제한을 받는다. Windows 2000 이후의 운영체제는 이러한 제한이 좀 줄어들었으나 여전히 불편함은 남아 있는데 MUI를 사용하면 타 언어 Windows 상황에서도 언어의 입/출력이 자유롭다. 예를 들어 중국어 Windows에서 '우리나라 만세'라는 한글을 어디서든 쉽게 쓸 수 있다.


 단점


 MUI는 웬만한 프로그램과 도움말을 다른 언어로 바꾸어준다. 하지만 모든것을 다른 언어로 바꾸어주진 못하며 일부분은 여전히 영문 Windows 상태 그대로가 된다. 예를 들자면 장치 관리자의 경우 언어를 바꾸어도 여전히 영문으로 남는다.


 글꼴 또한 기본적으로 영문 글꼴로 표시되는 경우가 많다. 그러므로 한글 또는 표준 일본어 글꼴에 익숙해진 사용자라면 익숙하지 않은 영문 글꼴의 모양에 거부감을 느낄 수 있다.

embedded에서 메모리 관리(퍼온글)

[응용] 메모리 관리
글쓴이 : 유영창 (2004년 09월 17일 오후 05:54) 읽은수: 6,520 [ 임베디드강좌/유영창 인쇄용 페이지 ]
APK006 메모리 관리
==============================================

1. 개요

이 문서는 ESP-NS에서 동작하는 응용 프로그램을
작성할때 메모리 할당과 해제및 메모리 처리에
대한 주의점을 소개합니다.

작성자 : 유영창 frog@falinux.com
작성일 : 2004년 9월 17일
수정일 :

관련된 ADK( Application Developer Kit ) 디렉토리

adk/sample/check_index
adk/sample/assert


2. 임베디드에서 메모리 관련 문제

임베디드 시스템은 사용시간에 따라서 크게 두가지로
나누어 볼수 있읍니다.

첫번째는 필요한 경우만 전원을 넣고 동작시켜 사용하는 경우로
동작 시간이 짧은 경우입니다. (참으로 고마운 시스템입니다.)

두번째는 모니터링 시스템같이 지속적인 제어가 필요하여
1년 365일 전원이 절대로 나가면 안되는 경우 입니다.

프로그래머 입장에서 보면 첫번째 방식을 좋아 하게 됩니다.
이건 무정전 시스템에 사용되는 프로그램을 작성하신분들이라면
온몸으로 느끼는 감정입니다. ( 해본 사람들은 압니다. ㅜㅜ )


시스템이 무정전으로 동작한다는 것은 여러가지를 고려 해야 합니다.
그중 으뜸은 메모리 누수 입니다.

C 로 작성하는 프로그램은 반드시 메모리에 관련된 문제 때문에
한번 이상은 반드시 고생하게 됩니다.

더구나 C 언어에 익숙하지 않으신 분이라면 포인터 참조에 관련된
수 많은 버그로 엄..청..난... 고생을 합니다.

그래도

납품하기전에 메모리 관련된 버그 문제점을 알게 되면 그나마
다행입니다.

그러나 프로그래머 입장에서 두고 두고 속썩이는 것중 하나가
장기간 동작하다 멈추는 경우입니다.

프로그램을 수정해서 버그를 잡았는지 확인하려고 하면
몇일씩이나 걸리기 때문에 프로그래머들을 미치기 일보
직전까지 만듭니다. ( 대부분의 경우 어디에서 발생했는지도
잘 모르죠.. )

이런 경험을 여러번 하다보면 나름대로의 방법론이 생깁니다.

이런 경험과 관련되어 메모리를 다루는 방법에 대한 몇(?)가지와
메모리 할당과 해제에 관련된 함수를 소개하려고 합니다.


3. 배열을 사용하라....

PC 프로그램을 작성하시던 분들이 임베디드 시스템에서
프로그램을 작성할때 가장 걱정되는 습관 중 하나가
메모리 할당과 해제를 아주 좋아 한다는 겁니다.

PC 시스템에서 작성하는 프로그램은 실장된 메모리가
많기 때문에 메모리 할당과 해제를 이용하면
유연한 프로그램이 가능해 집니다.

그..러..나..

임베디드에는 메모리 할당과 해제를 자주 이용하는 습관은
절대적으로 말리고 싶은 것 습관중 하나입니다.

보통 제품을 설계하는 분들이 개발자들에게 요구하는 것중
하나가 만능제품이죠...

그런데 이 만능은 프로그래머가 만능이 되어야 합니다.
이런 경우에도 적용될수 있고 저런 경우에도 적용될수 있고

마음약한 개발자들은 이런 요구를 수용합니다.

그러다 보니 개발해야 하는 프로그램 구조가 요구 사항에
가변적인 구조를 가지게 되죠..

결국 메모리 할당 구조와 리스트와 같은 자료 구조를 사용하게
됩니다.

이때부터 개발자는 머리털 빠지기 시작합니다.
( 제가 속빈 인간이 된 사연이 여기에 있읍니다. )
리스트구조와 메모리 할당은 시스템의 버그 원인 순위에
가장 상위 순위를 차지 합니다.

오랜 연륜을 가지는 개발자들은 일단 이런 영업 요구에
적절히 대항합니다.

그리고 어느정도 제품이 필요한 요구 사항을 제한합니다.
그리고 그에 맞게 프로그램을 개발합니다.

이때 이 분들이 작성한 프로그램을 보면 ( 무정전 제품에
들어가는 프로그램일수록 ) 전역변수와 배열을 많이 사용하게
됩니다.

이 전역변수와 배열을 사용하는 것은
프로그램을 처음 배울때 회피하라고 들었던 것인데
의외로 고수일수록 많이 사용합니다.
(심지어 goto 문을 남발하시는 고수도 많습니다. )

배열을 사용한다는 것은 일정한 크기를 갖기 때문에
확장성에 용이하지 않을 것 같은데
환경 파일로 모든 확장성을 고려하는 것은 임베디드
제품에 크게 의미가 없읍니다.

이미 한정된 크기의 메모리를 가지고 있는 시스템에
확장할수 있는 크기를 가지도록 프로그램을 작성한다는 것은
의미가 없읍니다.


배열을 사용하게 되면 다음과 같은 장점이 있읍니다

1) 메모리 할당과 해제와 관련된 버그가 없다.
2) 메모리 할당과 해제에 소모되는 시간 소모가 없다.
3) 인덱스 방식의 참조가 가능하므로 잘못된 포인터
참조에 의한 버그 가능성이 작다.
4) 시스템의 메모리를 효율적으로 사용할수 있다.
5) 참조 속도가 매우 빠르다.
6) 프로그램 소스 코드가 직관적이다.
( 포인터 참조 연산자가 얼마나 어려운 코드 형식을
작는지 다들 아시죠? )

등등의 장점이외에 더 있지만 생각이 안 나는 군요...

어쩄든 임베디드 장비에 사용되는 프로그램은 가급적 배열을
사용하시는 것이 좋습니다.

3. 가급적 상수를 매크로로 정의해서 사용하라

메모리 이야기에 왠 매크로 상수?

뭐 이렇게 궁금하게 생각하시는 분들이 있을 것 같은데...
이래야 고수 소리를 듣습니다. 소스에 숫자가 적게 보일수록 고수 입니다


예를 들어 보죠...

어떤 분은 프로그램을 이렇게 작성합니다.

char check_ids[300];

고수는 이렇게 작성합니다.

#define MAX_CHECK_IDS 300
char check_ids[MAX_CHECK_IDS];


이것은 나중에 확장성을 가지는 효과가 있고
접근하는 인덱스의 검사를 할 경우에 유용합니다.


또한 인데스 검사를 하는 경우에 유용합니다.
보통 프로그램을 작성할때 인덱스 접근에 대하여
다음과 같이 처리하면 좋습니다.


char get_date( int index )
{
#ifndef NO_CHECK_INDEX_FLAG
if( (index < 0) || (index >= MAX_CHECK_IDS ) )
{
dlp( "index over\n" );
exit(0);
}
#endif
return check_ids[index];
}

또는 아예 인덱스 검사를 하는 함수를 매크로로 만들어서 사용하는 경우도 있읍니다.

선언예)

#ifndef NO_CHECK_INDEX_FLAG
#define check_index(idx,max) {\
if( (index < 0) || (index >= MAX_CHECK_IDS ) ) \
{ \
dlp( "index over\n" ); \
exit(0); \
} \
}
#else
#define check_index(idx,max) {}
#endif


사용예)

char get_date( int index )
{
check_index(index,MAX_CHECK_IDS);
return check_ids[index];
}

3. 초기화를 꼭 하라

변수를 사용할때 특히 전역 변수를 사용할때
초기화를 하는 습관은 버그를 예방하는 효과가 있읍니다.
특히 포인터 형식의 필드변수를 포함하는 구조체가 있을 경우에는 특히나 그렇습니다.

초기화 값은 0으로 사용하는 것이 좋습니다.
포인터의

4. sizeof 함수를 즐겨 사용하라

메모리 복사나 초기화를 사용할 경우와 같이 배열이나 구조체의 크기를 구할 필요가
있을때 sizeof 를 자주 사용합니다.

귀찮아서 그냥 숫자를 주는 습관을 가진 분들이 있는데
이런 분들에게 sizeof 함수의 사용을 강력하게 권장합니다.

앞의 배열에서 초기화를 처리할때 다음과 같은 형식으로 처리하는 것이 좋죠...

memset( check_ids, 0, sizoef( check_ids ) );

복사할 경우에 역시 이런 식으로 사용하는 것이 좋습니다.
하지만 복사할 경우에 크기는 어느 것을 사용하는 것이 좋을까요?
권장하는 것은 앞에것을 사용 하는 것입니다

void copy_item( struct a *bdata )
{
struct a adata;

// 권장하는 예
memcpy( &adata, bdata, sizeof( adata ) );

// 별로 권장하지 않지만 좋은 예
memcpy( &adata, bdata, sizeof( struct a ) );


}


5. 포인터의 간접 인덱스를 사용할 때는 주의하라...

포인터 변수를 사용할때 포인터의 초보자들이 실수하는 큰 것
중에 하나는 다음입니다.
특히 하드웨어를 다룰때 많이들 실수 합니다.

char *app;
int *bpp;

app++;
bpp++:

이것은 1씩 증가 시키는 겁니다.
그런데 app 나 bpp 에 0x300000 이라는 주소값이 있다면
app는 0x30000 이 되지만 bpp 는 0x300004 가 된다는 것을
까먹습니다.

이것이 나중에 속썩일 경우가 많다는 점을 기억하십시오

(app+ 1) 과 (bpp+ 1) 도 마찬가지 입니다.
이런것은 매크로를 사용해서 선언할 경우 많이들 실수하는 겁니다.

예를 들어 하드웨어 레지스터를 접근하는 경우에

#define REG_A(x) (x + 1)
#define REG_B(x) (x + 2)

이런식으로 처리할때 매크로는 단순히 문자열 치환이기 때문에
위와 같은 문제가 발생할수 있다는 것입니다.


6. 스택변수 즉 로컬 변수를 조심하자

함수안에 선언하는 로컬 변수는 두가지 장점이 있읍니다.

선언이 간단하고 할당에 걸리는 시간이 없다는 것입니다.
그러나 이 로컬 변수는 버그의 온상이 되므로 주의할 필요가 있습니다.

예를 들어 다음과 같은 경우를 생각해 봅시다.

int test_func( char *tmpstr )
{
char buff[32];
int p;

p = strlen( tmpstr) ;

sprintf( buff, "ID:%s", tmpstr );
write_func( buff );

return p;
}

이 함수를 소스상에서 본다면 아무런 문제가 발생하지 않는 함수입니다.
그런데 이 함수는 두가지 문제점을 가지고 있읍니다.

만약 tmpstr 이 NUL 코드를 포함하지 않는다면?
또는 tmpstr 이 28 자 이상이 된다면 ?

아...

프로그램은 어떻게 동작할지 아무도 장담할 수 없읍니다.

다행히 세그먼트 폴트라도 발생해서 미리 알수있다면 좋지만
스택을 접근 할 경우에는 세그먼트 폴트가 잘 발생하지 않습니다.

경우에 따라서는 스택이 깨지기 때문에 엄한 곳으로 프로그램이 점프할수도 있고
다른 변수들이 수정될수도 있읍니다.

더구나 특별한 경우에는 두번 호출되어 도착한 함수가 꺼꾸로 리턴될때
중간 함수를 거치지 않고 리턴되거나 진행 루틴이 실행되지 않을 경우도 있읍니다.

또는 뒤에 선언된 변수들 값이 수정될수도 있읍니다.

이런 경우에는 스택 변수의 크기를 아끼지 않는 것이 가장 최선의 예방책입니다.
(물론 주의해서 작성하는 것이 더 큰 최선의 에방책이죠.. )

예를 들어 넘어온 크기보다 2 배나 3 배정도의 크기를 잡는 겁니다.

위의 경우에는 char buff[128] 정도로 선언하는 것이 안전합니다.

또는 버퍼의 뒷쪽에 임의 변수를 하나 두는 것도 요령인데
별로 추천은 하고 싶지 않군요


7. 자료구조를 사용한다면 검증된 라이브러리를 사용하라..

프로그램을 작성하다보면 배열보다 리스트와 같은 자료구조를
이용하는 것이 효율적일때가 있읍니다.

대표적인 것들이

스택이나 , 큐, 더블 링크드 리스트 , 리스트, 이진 트리 리스트
등등이 있읍니다.

이때 많은 분들은 직접 만들어 사용합니다.

그런데 이런 처리는 포인터를 사용해야 하고 저같이 논리에 약한
사람들이 만들면 버그가 살기에 좋은 환경을 제공합니다.

그래서 저는 인터넷에 관련 자료 구조용 공개된 소스를 이용하기를
권장합니다.

특히 소스포지에 가면 이런 자료 구조체 라이브러리들이 많이 있읍니다.
가급적 이렇게 공개되고 여러사람이 사용하고 있는 것을 이용하기를
바랍니다.

직접 만들면 피 봅니다... ㅜㅜ

8. 메모리 할당 함수들

C 에서 메모리를 할당하기 위해서 사용하는 함수들은 다음과 같습니다.

void *calloc(size_t nmemb, size_t size); // 할당 + 메모리 클리어
void *malloc(size_t size); // 할당
void free(void *ptr); // 해제
void *realloc(void *ptr, size_t size); // 재 할당 + 메모리 복사

이 중에서 가장 많이 사용하는 함수는

malloc 함수와 free죠...

하지만 malloc 함수보다는 calloc 함수를 사용하기를 권장합니다.
(저역시 malloc 함수를 자주 사용합니다만 ㅜㅜ )
그래도 malloc 함수를 자주 사용하게 되면 다음과 같은 처리를 꼭 해주시기를 바랍니다.

char *buff = NULL;

buff = malloc( 1000 );
if( buff != NULL )
{
memset( buff, 0, 1000 );
}
else
{
// 에러 처리 ( 보통은 프로그램을 죽인다. )
}

if( buff != NULL ) free( buff );


9. assert 함수


앞에서 malloc 함수를 처리할때 에러가 난 경우에 대한 처리가 귀찮죠?
이런 경우 사용하거나 기타 등등에 사용하면 좋은 함수가 assert 함수입니다.


이 함수는 #include <assert.h> 를 포함하고 사용하면 되는데
사용 문법은 간단합니다.

assert( 논리식 );

이 함수는 논리식이 참이면 특별한 것을 하지 않습니다.
그러나 거짓이면 에러를 표현 합니다.
파일명과 함수명 그리고 라인번호와 함께 문제가 된 값을 표현합니다.
그리고 프로그램을 죽입니다.( 으으 살벌.. )

보통은 포인터 변수의 값이 0이 되는 것을 방지하기 위해서 사용합니다.

assert 함수는 NDEBUG 가 정의 되어 있으면 아무런 것도 하지 않는
함수이므로 소스를 수정하지 않고서도 함수의 에러 처리를 무효화
할 수 있는 무척 좋은 함수 입니다.


<<<  댓 글 >>>

답장 korone.net (2004년 09월 18일 오후 12:48)
글 잘 읽었습니다.
글에 첨언해서 개인적인 견해를 몇가지를 이야기 해보자 이렇게 글을 씁니다.

우선,

3. 배열을 사용하라에서 장점에 대해서 열거해 주셨는데요.
1) 메모리 할당과 해제와 관련된 버그가 없다.
--> 버그가 없다고 말할 수 없습니다.
포인터 사용에서 발생되는 잘못된 주소의 엑스스로 인한 segmentation fault같은거와
마찬가지로, 배열의 인덱스를 잘못 엑세스할 수 있는 버그가 동일하게
발생될 수 있습니다. 가령 이런경우죠.
char aaa[10];
aaa[11] = 'd';
이런식으로 사용한다면 이 결과는 포인터와 마찬가지의 결과를 가지고 오게
됩니다.
지금 위 예제코드에서는 단순히 선언과 동시에 바로 아래에서 써서
이런 어이없는 경우가 어딨냐고 반문하실지 모르겠지만.
실제 프로그램 로직이 복잡한 상황에서 변수 선언과 실제 인덱스를 접근해서
사용하는 위치의 차이가 큰 경우가 많기때문에 충분히 저러한 상황이
발생할 수 있습니다
따라서, 배열로 하나 포인터로 하나 사용자가 주의를 기울이지 않으면 똑같은
결과가 나타날거라 판단됩니다.

2) 메모리 할당과 해제에 소모되는 시간 소모가 없다.
배열에 의한 메모리 할당의 가장 큰 장점은 해제 과정이 필요없다라는거죠
하지만, 이러한 장점보다는 포인터와 비교했을때의 단점이 더 많다고 보여집니다.
가령,
배열의 경우, 굉장히 않은 메모리를 할당할경우, stack에 생성되는것이
아니라, heap에 생성되므로 포인터와 동일한 연산을 수행하며,
포인터의 경우 메모리가 필요한 그 시점에 사용되어지고 유지되는 반면에
배열의 경우 이미 해당 작업 공간을 할당해 있으므로 메모리 사용에
제약이 가져올 수 있습니다.
또한, 비록 배열이 메모리 해제과정이 없으나, 실제 내부코드적으로
해제과정에 필요한 OP 코드가 생성되므로, 기본 연산은 거의 같다고
보아야 하므로 의미가 없을거란 생각이 됩니다.

3) 인덱스 방식의 참조가 가능하므로 잘못된 포인터
참조에 의한 버그 가능성이 작다.
--> 이건 1번 내용과 중복되는 설명입니다.

4) 시스템의 메모리를 효율적으로 사용할수 있다.
--> 2번 설명에서 밝혔듯이 전혀 효율적이지 않습니다.

5) 참조 속도가 매우 빠르다.
--> 속도가 빠른것은 인정할 수 없습니다.
어떠한 기준에 의해서 이러한 결과가 나왔는지 궁금합니다.

6) 프로그램 소스 코드가 직관적이다.
( 포인터 참조 연산자가 얼마나 어려운 코드 형식을
작는지 다들 아시죠? )


배열역시 포인터 입니다.
앞서 강좌에서 배열의 중요성을 이야기하는 관점이 잘못된 포인터 연산에
따른 문제점이 크기 때문이라고 하셨는데.
이때문에, 포인터 사용의 이점을 버린다는것은 잘못된것이라 판단됩니다.
포인터에 대한 명확한 이해를 하고 사용해야 하며,
또한, 문제점에 대해서 충분한 검증을 거치는 방법
가령, leak tracer나, memory profiler를 이용해서 테스트 & 검증해나가는
방법이 바람직하다 보여집니다.


6번에서 들으신 예를 보면
로컬변수의 장점이
"선언이 간단하고 할당에 걸리는 시간이 없다는 것입니다."라고 하셨는데
무엇과의 시간을 비교해서 할당에 걸리는 시간이 없다라는 의미인지요?
할당에 걸리는 시간은 전역변수나 로컬변수가 항상 같지요.
물론, 함수의 호출이 빈번할 수록 생성 해제의 과정이 반복되는점은
있지만, 변수 선언하나만 놓고 보았을때 같습니다.

그리고 아래에 예제 코드를 적어서 문제될만한 소지를 적어 주셨는데
이것은, 로컬변수의 문제점이 아닙니다.
Boundary check를 하지 않아 memory overflow가 나는겁니다.
이러한 문제점은 오래전부터 있어서 주로 해킹에 많이 이용되었는데.
이를 보완하기 위해,
strcpy 함수나 sprintf 함수같은경우 권고하지 않고
사이즈를 넘겨줄 수 있는 strncpy 함수나 snprintf함수를 사용하길
권고합니다. (리눅스 맨 페이지를 참고하면 자세한 사항을 알 수 있습니다.)


이상입니다.
고수님의 강좌글에 대해서 하수가 이런저런 토를 달아서 죄송합니다.

조병완
http://www.korone.net
[ 이글에 답장 | 본문에 답장 ]

답장 유영창 (2004년 09월 18일 오후 09:32)
3. 배열을 사용하라에서 장점에 대해서 열거해 주셨는데요.
1) 메모리 할당과 해제와 관련된 버그가 없다.
--> 버그가 없다고 말할 수 없습니다.

인정합니다 버그가 없다니
이런 실수를 .... 용서하십시오..


2) 메모리 할당과 해제에 소모되는 시간 소모가 없다.

배열에 의한 메모리 할당의 가장 큰 장점은 해제 과정이 필요없다라는거죠

--> 이 부분 역시 인정...


하지만, 이러한 장점보다는 포인터와 비교했을때의 단점이 더 많다고 보여집니다.

--> 인정 하기 힘듭니다.

이건 개발자 스킬과 연관이 있읍니다.
제가 이글을 쓴 목적중 하나가 개발 과정에 실수를 줄이는 부분입니다.
또한 장기간 사용되는 부분 때문입니다.

포인터를 쓰지 말라는 것이 아닙니다.

할당과 해제보다는 가급적 배열을 쓰기를 권장한다는 말입니다.

즉 프로그램머가 프로그램이 복잡할수록 할당과 해제과정에서
실수를 많이 합니다.

그래서 많은 디버깅 툴들이 주로 메모리 릭에 관련된 부분을 점검하는것으로 압니다.

할당과 해제에서 버그가 없는 분들은 이미 닳고 닳은 분들입니다.
그런분들은 이미 코드 작성시에 방어적인 코드로 작성합니다.
이부분만큼은 당해봐야 아는데
배열과 같은 부분은 사전 할당이 되기 때문에 손해가 적습니다.
특히 초보자들에게는

제가 보기에는 조병완씨는 고수로 보입니다. ^^

3) 인덱스 방식의 참조가 가능하므로 잘못된 포인터

이 부분은 조금 더 세밀하게 이야기 하면

함수에 매개변수로 인자를 넘길때 포인터를 많이 사용합니다.
그런데 이 포인터 참조 방식으로 넘길 경우에는
범위 초과에 대한 처리를 검증할 방법이 없는 경우가 많습니다.
그러나 인덱스로 넘기면 디버깅 과정에서 추적도 용이하고
검증 함수를 사용할수 있읍니다.
제가 이야기 한것은 이런 관점에서 인덱스를 사용하기를
권장하는 것입니다. 그리고 가급적 매크로 상수를 사용하기를
권하는 것이지요..

4) 시스템의 메모리를 효율적으로 사용할수 있다.

꺼꾸로 임베디드 시스템은 한정된 메모리를 가지고 있읍니다.
다시 말씀드리면 시스템 사양이 결정될 때 역계산에 편할수 있읍니다.

예를 들어

나는 이 시스템에 300명까지를 처리할수 있도록 하겠다와
나는 이 시스템에 상황에 따라서 300명에서 600명까지 처리하겠다는
그 구현방식에 따라서 버그 가능성은 매우 달라집니다.

이건 제 경험이기 때문에 다른 경험을 하시는 경우라면 해당되지 않겠지만
저는 프로그램을 작성할 때

ptrData = malloc( 100 );
if( ptrData == NULL )
{

}

하는 문장에서 많은 갈등을 합니다.

에러가 나면 어떻게 할까요?

그냥 프로그램을 종료하게 할까요?

동작중인 상태에서 무정전에 이런 처리 상황은 조금 불편하죠...
꺼꾸로

처음부터 모든 할당을 하고 시작한다는 조건이라면
저역시 말씀하신 의견에 찬성합니다.


5) 참조 속도가 매우 빠르다.
--> 속도가 빠른것은 인정할 수 없습니다.

실수 ( 헤헤 ) 제가 왜 이런말을 썼는지 다시 한번 생각해 보겠읍니다.
(분명히 이유가 있었는데 쩝 )

앞서 강좌에서 배열의 중요성을 이야기하는 관점이 잘못된 포인터 연산에
따른 문제점이 크기 때문이라고 하셨는데.
이때문에, 포인터 사용의 이점을 버린다는것은 잘못된것이라 판단됩니다.
포인터에 대한 명확한 이해를 하고 사용해야 하며,
또한, 문제점에 대해서 충분한 검증을 거치는 방법
가령, leak tracer나, memory profiler를 이용해서 테스트 & 검증해나가는
방법이 바람직하다 보여집니다.

--> 장기적으로는 저 역시 동의합니다.
하지만 저같이 게으른 사람은 그것을 느끼고 회피하는데 너무 많은 시간과
너무 많은 고생을 했읍니다.

배열로도 가능하면 그냥 배열로 처리하라는 것이 요즘 제 생각입니다.


그외 지적사항 역시 겸허히 받아 들입니다. ^^
[ 이글에 답장 | 본문에 답장 ]

답장 익명 (2004년 09월 20일 오전 10:59)
음... 로컬변수는.. 그냥 스택포인터만 조정하면 되니깐 빠른거 아닐까요? ㅡ,ㅡㅋ

하긴 생각해보니 임베디드 시스템에서 굳이 메모리를 놀릴필요가 없겠네요. ㅡ,ㅡ;

무의식적으로 보다 일반적이고 범용적인 코드를 짜려고 굳이 애써왔던 기억이 ㅡ,ㅡ;

이래서 쟁이라는 소릴 듣나. ㅡ,ㅡ;;;

아무튼 좋은 글 좋은 의견 잘 보았습니다. 확실히 크게 배운 것은 있습니다.
[ 이글에 답장 | 본문에 답장 ]

답장 korone.net (2004년 09월 20일 오후 12:12)
일반적으로는 그렇죠^^

로컬변수의 경우, 스택포인터만 조정하면 되니까,

하지만, 만약 스택에 담을 수 있는 변수의 길이를 넘게된다면 이야기가

틀려집니다.

만약, char tmp[100000];

이렇게 선언한다면, tmp 변수는 stack에 생기지 않고 heap에 생기게 됩니다.

임베디드 라서 굳이 메모리를 놀릴필요가 없겠다라는 말이

임베디드의 경우, 단가가 생명인데...

만약, 메모리를 64메를 올려놓고 프로그램을 짤때...

프로그램 메모리 사용을 효율적으로 해서 32메가로도 처리할 수 있다라면

이는 제품 단가를 줄일 수 있는 효과적인 방법이라 될 수 있다고 봅니다.

물론 강좌를 쓰신 유영창님께서도 밝혀주셨지만.

초보자를 위하고 어떠한 효율성보다도 안전성이 우선시 되는 임베디드

상황에서 나름대로 일리도 있다고 봅니다.

조병완
http://www.korone.net
[ 이글에 답장 | 본문에 답장 ]

답장 익명 (2005년 01월 18일 오후 03:43)
5) 참조 속도가 매우 빠르다.
--> 속도가 빠른것은 인정할 수 없습니다.

보통 프로그래밍시 동적으로 메모리를 할당 받는 경우는
대부분 리스트 이용을 위해서인데 배열이 리스트의 참조 속도보다 월등히
빠르다는 것을 인정할 수 없다는 것은 이해할 수 없군요.
배열의 참조는 상수 타임이며 리스트이 참조 시간은 빨라야 nlog(n)
time complexity를 갖게됩니다.
[ 이글에 답장 | 본문에 답장 ]

답장 익명 (2004년 09월 26일 오후 10:05)
전역변수보다 지역변수는 매번 비역변수의 공유영역을 지우고 다시 쓰는
절차가 컴파일러의 서브루틴 생성시 마다 발생하므로 결국 실행속도는
전역변수보다 지역변수가 느려집니다.
[ 이글에 답장 | 본문에 답장 ]

답장 익명 (2006년 12월 28일 오전 10:18)
포인터 사용에는 신중해야 합니다 에러의 주범 맞아요

Windows 에서 GTK+설치하고 갖고 놀기(퍼온글)

참고 사이트(http://cafe.naver.com/themaxkor.cafe?iframe_url=/ArticleRead.nhn%3Farticleid=29)

ms-window
에서 dev-c++(devcpp) gtk+ 으로 프로그래밍 하기


c도 초보고 gtk+도 초보입니다. 하지만 셋팅을 하는 동안의 과정을 간략하게 정리하고자 합니다.
dev-c++은 설치되었다는 전제하에서 설명하겠습니다. (참고로 dev-c++의 주소는 http://bloodshed.net/index.html 입니다.)
dev-c++을 한글환경에서 설명할 것이기 때문에 한글환경이 아니라면, 메뉴의 ‘tools’ -> ‘environment options’ 에서 ‘interface’ 탭에 있는’ language’에서 ‘korea(한국어)’를 선택하세요.

1. gtk+ 설치를 위한 사전 작업
gtk+는 다른 라이브러리들과 의존성을 가지기 때문에 사전에 의존성을 가지는 라이브러리들을 설치해주어야 합니다.

라이브러리 목록은 다음과 같습니다.

  • glib

  • atk

  • pango

  • zlib

  • libpng

  • libpixman

  • cairo

7개의 라이브러리를 먼저 설치해 주어야 합니다. 마지막에 있는 cairo는 바로 위 두개(libpng, libpixman)에 의존성을 가지므로 cairo를 설치하기 전에 두개의 패키지를 먼저 설치하여야 하고, libpngzlib에 의존성을 가지므로 zlib를 먼저 설치해야 합니다. 일단 이부분은 접어두고 총 7개의 라이브러리를 먼저 설치해야 합니다. 아래 부분에서 자세히 설명을 할테니 넘어갑시다.

설치를 위해 dev-c++에서 제공하는 패키지 업데이를 진행하면 됩니다.

메뉴의 ‘도구’ -> ‘프로그램 업데이트’를 선택하면 아래와 같이 하나의 창이 뜹니다.

select devpak server’에서 ‘devpaks.org community devpaks’를 선택합니다.

그리고, 아래의 ‘check for updates’를 클릭합니다. 그려면 프로그래스바가 진행되고 ‘available updates list’에 업데이트할 수 있는 목록이 작성됩니다. 그리고, ‘check for updates’는 ‘download selected’로 바뀌게 됩니다.

설치순서는 아래와 같습니다.

   1.
zlib
   2.l
ibpixman, libpng
   3.
atk, cairo, glib, pango


순서대로 각 항목에 체크를 하고 ‘download selected’를 눌러주면 됩니다. (한 항목에 두개 이상의 패키지가 있으면, 체크란에 두개 다 체크하고 ‘download selected’를 눌러주면 됩니다.) 간단한 대화창과 함께 설치가 완료되면, 이제 준비작업은 끝났습니다. (사실 준비작업이라 말하기는 좀 그렇지만...)


2.gtk+
설치
이제 gtk+를 체크하고 설치하면 됩니다.(download selected)
이제 gtk+를 프로그래밍을 하기 위한 준비는 끝났습니다. 그런데, 실행파일을 만들기 위해서는 dll 파일들이 필요합니다. 이 파일들을 gimp에서 다운받을 수 있습니다.

윈도우를 위한 gimp의 주소는 http://gimp-win.sourceforge.net/ 입니다. 다운로드 주소는 http://gimp-win.sourceforge.net/stable.html 입니다. 그런데...

gtk+ 2 runtime environment를 다운받아야 하는데, 목록이 두개가 있습니다.

저는 xp환경이라 위에 있는 (version 2.10.6 for windows 2000 and newer) 다운받고 압축풀고 설치를 시작하면 간단한 대화상자가 나오고, 설치를 하면됩니다.


3.gtk+ 프로그래밍 해보기
메뉴에서 ‘파일’ -> ‘새로 만들기’ -> ‘프로젝트’ 를 선택합니다. 그러면 아래와 같이 창이 뜹니다.
basic 탭에서 gtk+ application, 프로젝트명을 적당히 적어주고 언어는 c를 선택합니다. 그리고 확인을 누르면 gtk+ 프로그래밍을 할 수 있게 소스창이 구성됩니다.

메뉴에서 ‘실행’ -> ‘컴파일’ 그러면 컴파일이 되고 실행파일이 만들어집니다. 이제 gtk 프로그램이 완성되었습니다.

이제 http://gtk.org에서 튜토리얼을 보면서 이것저것 공부하면 됩니다. ^^
(그런데, ‘실행’ -> ‘실행’을 눌러주면 먹통일 때가 있습니다. 소스를 컴파일 한 곳에서 가서, 실행파일을 어떤 dll파일이 필요하다는 에러메시지가 뜹니다. 이때를 위해 조금전에 gtk+ runtime environment를 설치한 것입니다. ‘program files’- > ‘common files’ -> ‘gtk’ -> ‘2.0’ -> ‘bin’ 폴더에 보면 많은 dll파일들이 있습니다. 그곳에서 dll 파일을 찾아 실행파일이 있는 곳에 복사하고 실행파일을 더블클릭하면 이제 실행이 됩니다. 패스를 걸어주셔도 됩니다.)

vlc stream 구동하기

참조 : http://tldp.org/REF/VideoLAN-Quickstart/vlc-streamoutput.html

 < VLC Streaming 하기...>

 - 서버 측 -
 > /mnt/doc/mbs2/vlc/bin/arm-linux-vlc -d -I dummy -v0 v4l:/dev/video1:size=200x150:audio=0 --

sout='#transcode{vcodec=mp4v,deinterlace}:duplicate{dst=display,dst=std

access=udp,mux=ts,url=192.168.1.103}}'

또는
 > vlc -vvv video1.xyz --sout udp:client.example.org

또는
./arm-linux-vlc -I dummy -v --no-audio --ttl 12 v4l:/dev /video0:size=320x240 --sout '#std

{access=udp,dst=192.168.1.130}' -V X11 --color

또는
./arm-linux-vlc -I dummy -v --no-audio --ttl 12 v4l:/dev/video1:size=320x240 --sout '#duplicate

{dst=display}' -V X11 --color

또는
./arm-linux-vlc -I dummy -v --no-audio --ttl 12 v4l:/dev/video0:size=320x240 --sout '#std

{access=udp,dst=192.168.1.133}' -V X11 --color

또는
./arm-linux-vlc -I dummy -v --ttl 12

v4l:/dev/video1:size=160x120:adev=/dev/dsp:samplerate=32000:audio=0 --sout '#duplicate{dst=std

{access=udp,mux=ts,dst=192.168.1.133:1234}}' -V X11 --color

또는
./arm-linux-vlc -I dummy -v --ttl 12

v4l:/dev/video1:size=160x120:adev=/dev/dsp:samplerate=32000:audio=0 --sout '#transcode

{vcodec=mp4v,vb=1024,scale=1,acodec=mp3,ab=32,channels=2}:duplicate{dst=std

{access=udp,mux=ts,dst=192.168.1.133:1234}}' -V X11 --color

또는
./svlc -I skins2 -f -v --no-audio v4l:/dev/video1:size=160x120 --sout='#transcode{vcodec=mp4v,

vb=512,fps=10}:duplicate{dst=display,dst=std{access=udp,mux=ts,url=210.123.39.19:1234}' –color


 - 클라이언트 측 -

 > vlc -vvv udp:

Volatile이란?(퍼온글)

긁어온 사이트 (http://blog.naver.com/itsatan?Redirect=Log&logNo=100041107893)

volatile 키워드는 const 와 함께 변수의 성질을 바꾸는 역할을 하는데 이 둘을 묶어 cv 지정자라고 한다. const에 비해 상대적으로 사용 빈도가 지극히 낮으며 이 키워드가 꼭 필요한 경우는 극히 드물다. 어떤 경웅 volatile 이 필요한지 다음 코드를 보자.


int i;

double j;

for(i=0; i<100; i++) {

   j =sqrt(2.8) + log(3.5) + 56;

   // do something

}


이 코드는 루프를 100번 실행하면서 어떤 작업을 하는데 루프 내부에서 j에 복잡한 연산 결과를 대입하고 있다. j값을 계산하는 식이 조금 복잡하지만 제어 변수 i값을 참조하지 않기 때문에 i 루프가 실행되는 동안 j의 값은 상수나 마찬가지이며 절대적으로 변경되지 않는다. 루프 실행중에는 상수이므로 이 값을 매 루프마다 다시 계산하는 것은 시간 낭비이다. 그래서 제대로 된 컴파일러는 이 루프를 다음과 같이 수정하여 컴파일한다.


j =sqrt(2.8) + log(3.5) + 56;

for(i=0; i<100; i++) {

   // do something

}


i 의 값을 계산하는 식을 루프 이전의로 옮겨서 미리 계산해 놓고 루프 내부에서는 j값을 사용하기만 했다. 어차피 루프 내부에서 j값이 바뀌는 것이 아니므로 이렇게 코드를 수정해도 원래 코드와 완전히 동일한 동작을 할 것이다. 똑똑한 컴파일러는 프로그래머가 코드를 대충 짜 놓아도 속도를 높이기 위해 자동으로 최적화를 하는 기능을 가지고 있으며 이런 암묵적인 최적화 기능에 의해 프로그램의 성능이 향상된다.

그렇다면 위 두 코드가 정말로 완전히 동일할가 으심을 가져보자. j는 분명히 루프 내부에서 상수이므로 미리 계산해 놓아도 아무 문제가 없음이 확실하다. 그러나 아주 특수한 경우 최적화된 코드가 원래 코드와 다른 동작을 할 경우가 있다. 어떤 경우인가 하면 프로그램이 아닌 외부에서 j의 값을 변경할 때이다.

도스 환경에서는 인터럽트라는 것이유닉스 환경에서는 데몬, 윈도우즈 환경에서는 서비스 등의 백그라운드 프로세스가 항상 실행된다. 이런 백그라운드 프로세스가 메모리의 어떤 상황이나 전역변수를 변경할 수 있으며 같은 프로세스 내에서도 스레드가 여러 개라면 다른  스레드가 j의 값을 언제든지 변경할 가능성이 있다. 또한 하드웨어에 의해 전역 환경이 바뀔 수 도있다. 예를 들어 위 코드를 실행하는 프로세스가 두 개의 스레드를 가지고 있고 다른 스레드에서 어떤 조건에 의해 전역변수 j값(또는 j에 영향을 미치는 다른 값) 을 갑자기 바꿀 수도 있다고 하자. 이런 경우 루프 내부에서 매번 j값을 다시 계산하는 것과 루프에 들어가깆 너에 미리 계산해 놓는 것이 다른 결과를 가져올 수 있다. i루프가 50회째 실행중에 다른 스레드가 j값을 바꾸어 버릴 수도 있는 것이다.

이런 경우에 쓰는 것이 바로 volatile 이다. 이 키워드를 변수 선언문 앞에 붙이고 컴파일러는 이 변수에 대해서는 어떠한 최적화 처리도 하지 않는다. 컴파일러가 보기에 코드가 비효율적이건 어쨋건 개발자가 작성한 코드 그대로 컴파일 한다. 즉 volatile 키워드는 "잘난척 하지 말고 시키는 대로 해" 라는 뜻이다. 어떤 변수를 다른 프로세스나 스레드가 바꿀 수도 있다는 것을 컴파잉ㄹ러는 알수 없기 때문에 전역 환경을 참조하는 변수에 대해서는 개발자가 volatile 선언을 해야한다. 위 코드에서 j 선언문 앞에 volatile 만붙이면 문제가 해결 된다.


volatile double j;


이 키워드가 반드시 필요한 상황에 대한 예제를 만들어 보이는 것은 굉장히 어렵다. 왜냐하면 외부에서 값을 바꿀 가능성이 있는 변수에 대해서만 이 키워드가 필요한데 그런 예제는 보통 크기가 아니기 때문에 잘 사용하지 않는 기워드이므로 여기서는 개념만 익혀 두도록 하자.

시리얼에 관하여(퍼온글)

RS232C에 관한 하드웨어 자료도 많이 있습니다.
8051이나 8096,80196등에서 RS232를 지원합니다. CPU 에서 직접 데이타 신호가 나오는데
CPU 를 보호하기 위해 라인드라이버라는 것을 사용합니다. CPU는 5V전압이지만 라인드라이버는
12V 로 바꾸어서 전력을 공급합니다. 왜냐하면 멀리 보낼수 있게 하기 위함입니다.
MAXIM이라는 회사에서 나오는 제품을 많이 사용합니다. 직접 만들어도 됩니다.

MAX485, MAX490은 RS485 용인데 같이 검색해보시면 RS232용 라인드라이버도 나옵니다.
그리고 RS232프로토콜을 맞춰주는 칩으로 8251이 있습니다. CPU는 이 칩에 8bit 데이터 버스로
값을 읽거나 쓰고, 이 칩은 시리얼 통신을 해줍니다.

회사 웹사이트는 www.maxim.com입니다.


RS232C는 TCP/IP이전에 산업체의 표준이었습니다.
장비들을 만들때도 RS232 지원하게 만들었지요.
시리얼 통신에는 RS232말고도 여러개를 같이 묶을 수 있는 RS422,RS485가 있습니다.
TCP/IP도 시리얼 통신입니다. RS232C는 12V의 전압을 사용합니다. 통신선은 2개를 사용합니다.
하나는 신호, 하나는 Ground 입니다. 통신선간의 전압이 12V이면 0, 0V이면 digit 1로 여깁니다.
통신 속도는 300bps부터 115k까지 다양하게 사용합니다. 단 통신선의 길이가 길면 통신속도
높은 거 사용못합니다. 노이즈가 통신선에 발생하기 때문입니다.
자세한 내용은 아래의 글과 웹사이트를 참고하세요

http://www.arbank.co.kr/ 여기 사이트 가면 RS-232 와 IrDA연결 장비가 있습니다.
저는 PDA의 적외선 포트를 이용해서 PC와 연결해보려고 찾아보다가 말았답니다.
도움이 되면 좋겠습니다.
아래는 참고 자료들이예요... ^^;;

RS232C의 개요
Serial Port
아스키코드로 'H' 문자는 16진수로 48 이고 이진수로 01001000 이다. 1 Byte(8bit) 이다.
또한 '1' 과 '0'의 일련(+5 V와 0V purse)으로 데이터버스에서 전송될 수가 있다.
컴퓨터 내부에서는 병렬동기신호 방식(parallel synchronous signals)으로 byte를 전송한다.
병렬전송은 아주 짧은 거리에서만 잘 작동한다. 하지만 신호가 빨라질수록 병렬 전송을 이용하는 것은
더욱더 힘들어진다. 아주 먼거리를 전송시키기 위해서는 송신기에 의해 직렬전송신호로 바뀌어야 한다.
그리고 수신측 컴퓨터의 수신기에서는 병렬신호로 바뀌어 져야 한다.
이 작업을 하기위해서 우리는 UART(Universal Asynchronous Receiver Transmitter)라 불리우는 특별한 병렬 칩을 사용하게 된다.UART는 때로는 ACIA(Asynchronous Communications Interface Adapter)라고도 불리운다.
인텔 마이크로프로세서 칩번호는 8250 또는 16550이다. UART는 병렬 데이터 비트를 받아서 직렬연속비트로 변환한다.또한 반대편의 UART는 전송정보를 반대로 변환시킨다.
UART(Universal Asynchronous Receiver Transmitter)
주 로 PC에 사용하는 것으로 1981년 IBM - PC에서 IN8250이 머더보드상에 탑재해 모템이나 Serial Printer와의 데이터수수에 사용되고 이후 IBM-PC에서는 8250의 사용방법과 BIOS기능은 아키텍쳐기능의 양면에 있어 UART는 Defacto Standard가 되었다.
정확히 PC의 IO Card의 IO chip을 말하며 이 IOchip은 프로피디스크 컨트롤러, IEEE1294패러럴포트, 범용칩셀렉터, 구성레지스터, 플러그앤플레이서포트 회로, 파워다운로직회로 그리고 두개의 UART 시리얼 인터페이스(16C550 or 16C450)이 있다. 그중 UART2번째 포트에서 범용적외선인터패이스를 지원하도록 구성되어있다.
원칩마이크로 프로세서도 내장 - 8051, MC68HC11, Z8등 DSP에서는 채용하지 않는다.- 왜냐하면 dsp는 대규모의 동기시스템이므로 시리얼데이터는 비동기입력에 응답하는 것은 곤란하다.
MAX232나 MAX485와의 인터페이스를 담당하는 MCU 의 PIN쪽을 생각하면 된다.
EIA-574/232 -> IrDA의 쌍방향 변환 어댑터를 전/기/98/1/107에서 볼 수 있다.
퍼 스널컴과의 RXD 와 TXD의 신호를 MAX233으로 다시 그 신호를 8051의 TXD 와 RXD 로 주고 Port 1.0에서 1.4과 P3.2까지를 다시 MAX 3100의 순서대로 DIN, DOUT, SCLK, /CS, /IRQ로 배정하고 MAX3100의 TX와 단자를 IR LED와 IR PD로 배정한다.
8bit이고 패리티비트를 사용하지 않으며 한번에 시작하고 멈춘다.
한 마디로 PC의 RS-232포트를 생각해도 괜찮을 것 같다.

RS-232C통신이란
RS232C( Recommended Standard system for serial communications의 한 부분)
1969년 미국 전자공학협회에서 제정한 통신 인터페이스 규약의 하나로 프린터, 모뎀 등 각종 주변장치를 연결하는데 주로 사용된다. 이러한 시리얼 포트에 비동기 통신을 위해서는 이미 언급했지만 UART가 반드시 필요하다.
현재는 속도 측정단위인 bps(bits per second)를 쓰지만 예전에는 Baud( signal change per second)를 썼었다.
RS-232
19.2kbps 15M
RS-422
10Mbps 15M 이상
RS-423
100Kbps 15M 이상
RS-449
2Mbps 15M 이상
위 의 모든 통신 방식은 연결을 위해 최대 25개의 다른 선 구성을 가진다. 이 접속에는 "25핀 DSUB 커넥터"나 "9핀 미니 DSUB 커넥터"라 부르는 커넥터가 일반적으로 사용되고 있다.(RS-232C의 C는 1987년 어떤 전기적인 특성을 설명하기위하여 붙여졌다.)
PC끼리 연결을 하고자 한다면 다음 그림과 같이 하면 된다.
TXD는 수신신호이고 RXD는 전송신호이다.SG는 접지시그날이다.위의 연결은 NULL 모뎀 연결이라고한다.
핀별 설명
9핀 커넥터
25핀 커넥터
신호 명칭
간단한 사용법
1
8
CD(Data Carrier Detect)
입력, 사용하지 않는다
2
3
RD(Receive Data)
입력, 상대방 TD에 접속
3
2
TD(Transmit Data)
출력, 상대방 RD에 접속
4
20
DTR(Data Terminal Ready)
출력, 사용하지 않는다
5
7
SG(Signal Ground)
그라운드, 상대방 SG에 접속
6
6
DSR(Data Set Ready)
입력, 사용하지 않는다
7
4
RTS(Request to Send)
출력, 상대방 CTS와 접속
8
5
CTS(Clear to Send)
입력, 상대방 RTS와 접속
9
22
RI(Ring Indicate)
입력, 사용하지 않는다
참고
http://home.opentown.net/~sienes/Contents/UART.htm
http://mole.chm.bris.ac.uk/~cijpm/CM1/lt6.htm
http://win98.co.kr/comunder/0806.htm
http://plaza.snut.ac.kr/~auto/academic_place/under1.htm
http://comdol.pe.kr/study/eletronic/lec02/pic14.html



시리얼 통신의 기초 지식 : http://www.sciencesoftware.co.kr/TALtech/Tutorials/intro_sc/intro_sc.htm

RS-232C 통신 설치 연결도 : http://lcdups.co.kr/rs232c.html

RS-232C 시리얼 통신 프로그래밍 (C) : http://cprogram.home.uos.ac.kr/cplecture/cpch30.htm



그리고



EIA-232-D 프로토콜

EIA-232-D 프로토콜은 DTE와 DCE간의 인터페이스 기능을 한다.

1. DTE(Data Terminal Equipment, 데이터 단말 장비)

: 터미널, 컴퓨터와 같은 디지털 데이터 처리 디바이스

2. DCE(Data Circuit-terminating Equipment, 데이터회선 종단 장비)

: 모뎀과 같이 DTE와 전송시스템 간에 중재를 한다.

3. 물리층 프로토콜의 특성

1) 기계적 특성: DTE와 DCE간의 실제 물리적 연결에 관한 특성

2) 전기적 특성: 전압레벨과 전압변동의 타이밍에 관련

3) 기능적 특성: 교환회선에 의미를 할당, 데이터, 제어, 타이밍, 접지로 분류

4) 절차적 특성: 기능적 특성에 따라 전송 데이터에 일어나는 사건의 순서

4. 전송모드

1) 평형모드: twisted pair 이용하는 전화시스템

① 동작: 2개의 도체로 된 평형전송 회선으로 신호를 전달(한 도체로 나가 다른 도체로 돌아옴)

② 차등신호법: 디지털에서 부르는 것으로 이진 값이 두 도체 사이의 전압차의 방향에 따른다

③ 특징: 불평형 전송보다 잡음이 적고 면역성이 크다.

 

2) 불평형모드

① 동작: 신호의 전송에 한 통로를 이용하고 복귀통로는 접지가 이용한다.

② 특성: EIA-232-D에서 사용시 동축케이블, 짧은 거리로 제한된다.

EIA-232-D 상세

1. 기계적 특성: DB-25 커넥터라 불리는 25또는 9핀 커넥터

2. 전기적 특성

1) 기능: DTE와 DCE간의 신호법을 기술

2) 해석: 공통접지를 기준으로 3V이하이면 1로, +3V이상이면 0으로 해석

3) 제한: 신호전송율 20kbps이하, 거리 15m이하

3. 기능적 특성

1) 데이터회선(4): 각 방향마다 하나씩의 회선이 있어 전이중방식으로 동작한다. 반이중방식에 이용하는 2개의 보조회선이 있다.

2) 제어회선(14)

① 비동기전송에서 1차 채널상의 데이터전송 제어(6)

② 동기전송에서 1차 채널상의 데이터전송 제어(2)

③ 신호질 검출기회선

2차 채널의 사용제어(3)

⑤ 루프백 시험용(3)

3) 타이밍회선(3): 동기식 전송에 클럭펄스 제공

4) 접지와 차폐(2)

① 접지: 모든 데이터 도선에 대한 리턴 회선으로 작용

② 차폐: DTE측에서 차폐된 케이블의 연결을 위한 것

5) 루프백 제어: 통신이 끊어졌을 때 원인을 찾는다.

로컬 루프백: 로컬 인터페이스와 로컬 DCE의 기능을 시험한다.

원격 루프백: 전송채널과 원격 DCE의 기능을 시험한다.

4. 절차적 특성: action-reaction방식에 기반하여 회선이 사용되는 순서를 정의한다.

5. 널모뎀

정의: 두 디바이스 간의 거리가 매우 가까워서 두 DTE가 직접 신호를 주고받을 수 있으면, DTE가 자신의 모뎀에 연결되어 있는 것처럼 단자를 상호연결한다.


Ram Disk vs MTD(퍼옴)

글쓴이 : 유영창

=========================================================
   램디스크를 쓸것인가? MTD를 쓸것인가?
=========================================================

1. 개요

이 문서는 ESP-NS에서 동작하는 응용 프로그램을 실장하는 파일 시스템으로 램디스크를 사용할것인지 아니면 MTD를 사용할 것인지를 결정할때 도움을 주기 위한 문서입니다.

2. 파일 시스템

응용 프로그램이 동작하기 위해서는 파일 시스템이 반드시 필요합니다.

윈도우즈 프로그램만 하시던 분들은 이 파일 시스템에 대한 관심이 필요한 시점은 딱 한번! 윈도우 운영체제 설치시에만 필요합니다. NTFS 라는 이름을 가지는 것이 바로 그것입니다. (물론 설치가 빈번해지면 이 한번이 여러번 되죠 ㅜㅜ)
그래서 파일 시스템이 왜 필요한지도 모르고 살아갑니다. (부럽습니다.)

그러나 임베디드 시스템에 리눅스를 사용하고 응용 프로그램을 적재하는 과정에서는 이 파일 시스템에 대한 이해가 필요합니다. 더불어 램디스크나 MTD와 같은 블럭 디바이스 드라이버에 대한 이해도 필요합니다.

솔찍히 저도 윈도우 프로그램만 작성하다가 리눅스로 넘어 오면서 뭐 이딴것을 알아야 하는가에 대한 고민도 했읍니다. 하지만 어쩔 수 없이 알아야 하는 내용입니다.

우리는 두가지 개념에 대하여 이해가 필요합니다.

첫번째는 저장 매체를 다루는 블럭 디바이스 드라이버와 관련된 내용입니다.

두번째는 저장 매체에 파일을 저장하고 관리할 수 있는 파일시스템에 대한 내용입니다.


2. 블럭 디바이스 드라이버

임베디드 시스템은 전원이 소실되어도 데이터를 지속적으로 보관하거나 보드에서 동작하는 응용프로그램( 펌웨어라고도 합니다. )을 담고 있는 물리적인 매체가 필요합니다.

이런 저장 매체에는 ROM을 사용하기도 하고 RAM을 사용하기도 하고 플래쉬 메모리를 사용하기도 하고 하드 디스크를 사용하기도 합니다.

그러나 이 글은 ESP 보드에서 동작하는 응용 프로그램울 작성하는 방법에대한 내용이므로 RAM 과 플래쉬 메모리와 관련된 이야기만 하겠읍니다.

여기서 의문을 제기하시는 분이 있을 겁니다.

"RAM 이라니? 그건 전원이 나가면 내용이 소실되는데?"

맞습니다. RAM은 전원이 나가면 내용이 소실됩니다.
그러나 이야기 전개상 필요하므로 넘어 갔으면 좋겠읍니다.
( 싫으면 이 글을 더 이상 읽지 마세요.. 쩝 )

리눅스에서 저장 장치를 다루는 디바이스 드라이버가 블록 디바이스 드라이버 입니다. 물론 이 저장 장치라는 것은 소프트웨어 관점으로 보았을때 하드 디스크와 같은 특성을 가지는 것들을 말합니다.

임베디드 리눅스에서 블록 디바이스 드라이버로 주로 사용되는 것에는 램디스크,MTD 가 있읍니다.

램디스크는 램을 이용해서 하드 디스크를 흉내내는 것입니다.
MTD는 플래쉬 메모리를 이용해서 하드 디스크를 흉내 내는 것입니다.

램디스크는 보드에 장착된 램(RAM)을 이용합니다.

MTD는 NOR 나 NAND 플래쉬 메모리를 이용하여 구현합니다

여기서 잠깐 저장 장치로서 NOR 와 NAND 플래쉬중 어떤 것이 우수한가를 살펴 봅시다.

제 경험상 NOR 계열의 플래쉬보다는 NAND 계열의 플래쉬가 저장장치로 사용하기에는 더 좋습니다. 데이터를 써 넣는 속도가 더 빠르고 데이터를 읽어 오는 속도나 용량 대비 단가가 더 싸고 안정적입니다.

하드웨어 설계자 입장에서 보면 NAND 계열의 플래쉬보다는 NOR 계열의 플래쉬가 더 유리합니다. 왜냐하면 부팅 롬으로 바로 이용이 가능하고 부가 회로가 별로 필요 없기 때문입니다.

대량 양산 보드에서는 가격적인 문제라면 NAND가 유리합니다. 반면 양산 생산 관리에서는 NOR가 유리합니다

ESP 보드는 NAND 플래쉬를 사용한 저장방식입니다
(저희 회사 보드들이 다 그렇죠... 다 경험의 산물입니다. )

아무래도 이 글은 ESP 와 관련된 보드를 중심으로 설명하는 것이므로 NAND 플래쉬 관점에서 이야기 해야 겠죠...

어찌 되었든 ESP 보드는 두가지 블럭 디바이스 드라이버를 사용할 수 있읍니다.

1) 램을 이용한 램디스크 시스템
2) NAND 플래쉬를 이용한 MTD 시스템

ESP 보드는 처음 판매될 때 램디스크를 이용하고

/app 디렉토리에 NAND 플래쉬에 접근할수 있는 MTD 시스템이 마운트되어 있읍니다.

이 두가지 장치는 모두 동시에 사용 가능합니다.



3. 파일 시스템

램디스크나 NAND 플래쉬를 이용한 MTD가 있다고 해서 파일을 읽거나 쓸수는 없습니다.

왜냐하면 해당 저장 장치에 어떤식으로 파일을 기록해야 하는 방법이 없기 때문입니다

즉 블록 디바이스 드라이버라는 것은 단순히 섹터단위로 데이터를 어떻게 기록하고 읽는 방법만 제공하기 때문입니다.

그러나 파일을 읽고 쓰기 위해서는 디렉토리도 관리해야 하고 파일 명이나 기타 정보도 관리해야 합니다.

이렇게 섹터단위로 읽고 쓸수 잇는 장치에 파일을 저장하고 읽도록 하는 것이 파일 시스템입니다

리눅스는 이런 파일 시스템으로 사용할수 있는 것은 여러가지가 있읍니다. 가장 대표적인것인 EXT2 라는 파일 시스템입니다.

ESP 보드에서는 EXT2 파일 시스템은 램디스크에 사용하고 있습니다.
PC 의 하드 디스크에는 최근에는 EXT3 가 가장 많이 사용되고 있습니다.
( 배포판이 이 파일 시스템을 사용하기 때문이 가장 큰 이유죠 )

이외에도 ESP 보드에서는 YAFFS 파일 시스템을 사용합니다.
이 YAFFS 파일 시스템은 리눅스에 정식으로 포함되어 있지 않습니다.
하지만 NAND 파일 시스템에 사용하기에는 제 경험상 이놈이 딱! 입니다.

NAND 플래쉬나 NOR 플래쉬에 사용되는 것으로는 JFFS2 가 있습니다.
NOR 플래쉬라면 아무래도 JFFS2 가 더 좋습니다.

ESP 보드에는 YAFFS 를 사용하기 때문에 JFFS2 에 대한 이야기는 하지 않겠지만 여러분에게 주의 사항은 이야기 하고 싶습니다.

JFFS2는 치명적인 결함이 있습니다. ( 지금은 고쳐졌는지 잘 모르겠읍니다. )
JFFS2에 사용하는 파티션이 2에 지수 단위로 나누어 지지 않으면 지속적인 파일을 기록하는 경우에는 리눅스 시스템이 죽어 버립니다.
원인은 저도 잘 모르겠읍니다. ㅜㅜ

또 플래쉬의 크기가 커지면 시스템 메모리를 많이 사용해 버립니다.
32M 정도의 NOR 플래쉬라면 큰 문제는 없지만 그 이상의 메모리를 사용하게 되면
문제가 될 소지가 있읍니다.

이 점은 JFFS2 파일 시스템을 사용하시는 분들은 조심하시기 바랍니다.

어쨌든 여기서 파일 시스템을 정리하면 다음과 같습니다.

RAM               : 램디스크 : EXT2
NAND 플래쉬 : MTD        : YAFFS


이것이 ESP 보드에 사용되는 파일 시스템의 구성 정리 입니다.


4. 램디스크와 램디스크 이미지

램디스크는 램에 하드 디스크처럼 저장을 할수 있도록 합니다.

그..런..데

램은 전원이 나가면 소실됩니다.

원래 램디스크는 다음과 같은 과정을 거쳐야 사용 가능합니다.

[root@ESP /] mkfs.ext2 /mtd/ram0
[root@ESP /] mount /mtd/ram0 /test

이렇게 하면 /test 디렉토리에 파일을 만들면 램디스크에 데이터를 저장할 수 있습니다.

그러나 보드에 전원이 나가면 데이터는 소실됩니다.
또 보드가 부팅될 때 램디스크를 루트로 사용하려고 해도 파일 시스템도 안 만들어져 있고 내용도 없기 때문에 루트 파일 시스템으로 사용이 불가능합니다.

그래서 이런 점을 해결하기 위해서 램디스크 이미지라는 것을 사용합니다.

램디스크 이미지는 램디스크에 EXT2 포맷으로 만들고 필요한 파일을 모두 넣은 후에 램 디스크의 내용을 모두 파일 형태로 복사한 파일입니다.
리눅스 커널은 이 파일의 내용이 있는 위치를 지정하면 해당 내용을 부팅시에 램디스크에 모두 옮겨 넣습니다.
이때 커널은 램디스크 이미지가 압축되어 있다고 가정합니다.

ESP 에서는 이지부트가 이 압축된 램디스크 이미지를 특정 램에 복사해 놓고 커널 부팅전에 커널에 이 이미지 데이터가 어디 잇는지를 알려 줍니다.

그래서 커널은 부팅 후 루트로 램디스크 이미지를 사용 할수 있는 겁니다.

5. 램디스크를 사용할 것인가 아니면 MTD에 YAFFS를 사용할 것인가

이제 결론적으로 이 두가지에 대한 결론을 내려 봅시다.

램디스크 이미지를 이용하여 응용 프로그램을 탑재하고 사용할것인가 아니면 YAFFS 파일 시스템을 이용하여 NAND 플래쉬를 이용하여 응용 프로그램을 탑재하고 사용할 것인가..

이 두가지 중 하나를 선택하기 위해서는 각각의 장단점을 알아야 합니다.

5.1 램디스크 이미지를 이용하는 방법의 장점 과 단점

램디스크 이미지를 이용하는 장점은 딱! 두가지 입니다.
그 외에는 장점이 별로 없습니다.

첫번째 장점은 시스템이 안정적이라는 것입니다.

램디스크는 응용 프로그램이나 기타 등등의 이상이 있을 때 파일 시스템을 박살내더라도 전원만 껐다가 키거나 리부팅이 되면 원위치가 됩니다.

두번째 장점은 많은 수량의 보드를 제작할때 쉽게 전체 시스템을 설치할 수 있습니다.
즉 부트로더 + 커널 + 램디스크 이미지 를 한꺼번에 실장하면 됩니다.

그...러...나

가장 큰 단점은 개발시에 무척 불편하다는 것입니다.
항상 램디스크 이미지를 매번 만들어야 합니다.
(물론 nfs 파일 시스템을 이용해서 개발하고 나중에 한번에 써 넣어도 되죠 )

또 다른 단점은 응용 프로그램의 업그레이드를 하려면 골치아프다는 것입니다.
원격지에서 자동으로 응용 프로그램을 업데이트 하려면 결국 램디스 이미지를 통째로 바꾸어야 하는데 이게 만만한 작업이 아닙니다.

5.2 YAFFS 파일 시스템을 사용할때의 장점 과 단점

YAFFS 파일 시스템을 이용해서 개발할 때 가장 큰 장점은 개발할때 편리하다는 것입니다.

하드 디스크에 저장하는 것과 같은 기분으로 응용 프로그램이나 기타 데이터를 그대로 복사하여 사용하면 됩니다.
/etc/ 나 기타 등등에 필요한 파일을 복사만 하면 전원을 껐다가 켜도 보관이 됩니다.

이에 반해 많은 제품을 양산하기에는 일일히 복사해야 하는 단점이 있고 파일 시스템이 깨지는 경우 ( 거의 없읍니다만 ) 시스템이 사용 불가능해 지는 단점이 있습니다.

5.3 결론

저는 램디스크 이미지 형식을 사용하는 것보다 MTD에 YAFFS 을 이용하기를 권합니다.

개발시에 편하고 생각보다도 안정적이기 때문입니다.

실제로 제가 YAFFS에서 데이터가 깨지는 경우는 본적이 없읍니다.
(하드웨어가 고장나서 깨지는 경우는 보았습니다. ㅜㅜ )

가장 큰 이유는 저 같은 게으른 사람에게는 개발시에 무척 편리하다는 것입니다.


6. ESP 보드에서 램디스크를 MTD 시스템으로 자동으로 바꾸는 명령

ESP 보드는 초기 생산시에 루트 파일 시스템을 램디스크를 이용하도록 되어 있읍니다.

이것을 NAND 플래쉬가 루트가 되도록 하기 위해서는 다음과 같은 명령을 사용합니다.

[root@ESP /]$ cd /root
[root@ESP /root]$ ./mkflashroot

이 명령을 사용하면 루트 파일 시스템을 NAND 플래쉬로 사용하도록 해 줍니다.

이 명령은 스크립트로 다음과 같은 내용을 가지고 있읍니다.

=====[/root/mkflashroot]======================================
#!/bin/sh
echo "wait about 20sec ..."
cd /
echo "copy /bin"
cp -pfR bin /app
echo "copy /dev"
cp -pfR dev /app
echo "copy /etc"
cp -pfR etc /app
echo "copy /home"
cp -pfR home /app
echo "copy /lib"
cp -pfR lib /app
echo "copy /root"
cp -pfR root /app
echo "copy /sbin"
cp -pfR sbin /app
echo "copy /tmp"
cp -pfR tmp /app
echo "copy /usr"
cp -pfR usr /app
echo "copy /var"
cp -pfR var /app
mkdir /app/proc
mkdir /app/mnt
mkdir /app/mnt/nfs
mkdir /app/mnt/cf
mkdir /app/mnt/mmc
echo "mkdir ...done"

cp -f /root/.fstab /app/etc/fstab
cp -f /root/.rc.local /app/etc/rc.d/rc.local

echo "change EZBOOT->set->Kernel Command"
echo " [noinitrd root=/dev/mtdblock2 console=ttyS02,115200]"
echo " "
=====[/root/mkflashroot]======================================


무식하죠?

마지막에 표출되는 메세지인

change EZBOOT->set->Kernel Command
[noinitrd root=/dev/mtdblock2 console=ttyS02,115200]

는 이지부트 명령 모드 에서 set 명령을 이용하여 커널에 전달되는 명령 문자열에

noinitrd root=/dev/mtdblock2 console=ttyS02,115200

내용을 포함라는 것입니다.

리눅스 커널의 이해(4)(퍼온글)

리눅스 커널의 이해(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를 올리고 나오는 개념이다. 좀 더 구체적으로 보자. 문제가 되는 영역에 들어가고자 할 땐 다음과 같은 루틴을 수행한다.


while(1) {
     if(flag]0) {
          flag--;
          break;
     }
}


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

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


flag++;


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

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



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


while(1) {
   cli;
   if(flag]0) {
        flag--;
        sti;
        break;
   }
   sti;
}


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


cli;
flag++;
sti;


이 두 루틴을 이용하여 [시스템 콜 루틴 내부의 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) 항목]을 각각 다음과 같이 처리해 본다.


while(1) {cli; if(flag]0) {flag--; sti; break;}sti;}
디바이스를 사용하고 있으면
데이터를 데이터 큐에 넣고 나간다
cli; flag++; sti;

while(1) { cli; if(flag]0) {flag--; sti; break;}sti;}
데이터 큐가 비어 있으면
디바이스를 다 사용했다고 표시하고 나간다
데이터 큐가 비어 있지 않으면
데이터를 하나 꺼내서 디바이스 버퍼에 쓰고 나간다
cli; flag++; sti;


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


cli; if(flag]0) {flag--; sti; return;} sti;


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

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

이상에서 [디바이스에 쓰기 동작]에 대하여 Uni-Processor 상에서 발생할 수 있는 동기화 문제와 그에 대한 해결책을 알아보았다. 다음 기사에는 일단 [디바이스에 쓰기 동작]에 대한 구체적인 예를 들여다보기로 하자.

리눅스 커널의 이해(3)(퍼온글)

리눅스 커널의 이해(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 환경에 따라 발생할 수 있는 동기화 문제의 여러 가지 패턴을 살펴보고 그에 대한 해결책을 알아보기로 하자.

리눅스커널의 이해(2)(퍼온글)

리눅스 커널의 이해(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 초기화도 들여다 보았다. 다음 기사에서는 리눅스 커널이 구체적으로 어떻게 구현되었는지 소스 수준에서 살펴 보기로 하자.
prev 1 2 3 4 5 next