2009년 11월 25일 수요일

vi 치환

 
초강력 에디터 vi, emacs

구글 검색을 통해 검색되는 관련 자료들을 모아모아 보았다. 앞으로 더 좋은게 발견되면 추가할 것이다.

JimyLinux







http://mwultong.blogspot.com/2006/09/vim-vi-gvim-find-and-replace.html

[문자열 찾기 바꾸기] Vim (Vi), Gvim 리눅스 텍스트 에디터 - 찾기/치환 Find and Replace

GVim 또는 Vim에서 문자열을 찾거나 치환(바꾸기)하는 방법입니다.

문자열 찾기(Find)

* 현재 문서를 편집중이라면, 키보드의 Esc 키를 눌러 편집모드에서 빠져나옵니다.

* 키보드의 슬래쉬(/)키를 누르고, 찾을 문자열을 입력합니다. 만약 foo 라는 문자열을 찾는다면
/foo
이렇게 하면 됩니다.

그런데 /키는, 위에서 아래로 찾는 것입니다.

아래에서 위쪽 방향으로 찾으려면 물음표(?)키를 사용합니다. 현재 커서 위치의 위쪽에 있는 foo 라는 문자열을 찾으려면
?foo
라고 하면 됩니다.


다음 문자열 계속 찾기

다음 문자열 찾기는 소문자 n 입니다. 만약 foo 가 여러 개 있다면, 아래쪽 foo들을 계속 찾게 하는 것입니다.

역방향으로 계속 찾으려면 대문자 N 을 누릅니다.


대소문자 구분 없이 찾기

Vim은 대소문자를 구분하여 찾기에 불편합니다. Vim 설정 파일인 .vimrc 또는 _vimrc 파일에,
set ignorecase
이런 줄을 삽입하면 대소문자 구분 없이 찾기를 할 수 있습니다. (▶▶ [.vimrc] Vim / Gvim 설정 파일 예제 - 리눅스 텍스트 에디터 참고)



문자열 바꾸기(치환)

* 현재 문서를 편집중이라면, 키보드의 Esc 키를 눌러 편집모드에서 빠져나옵니다.

* 콜론(:)을 누르고 %s/foo/bar 라고 하면 모든 foo라는 문자열이 bar로 한꺼번에 치환됩니다.

다음과 같이 c 라는 옵션을 붙이면
:%s/foo/bar/c
바꿀 때마다 바꾸어도 좋은지 물어보기에 더 안전합니다. y를 누르면 바꾸고, n을 누르면 다음으로 건너뛰고, a를 누르면 모두 바꿉니다.


:%s/<foo>/bar

이렇게 하면 정확하게 foo에 일치될 때만 바꿉니다. 즉 foo는 바꾸지만, foo 앞뒤로 다른 문자열이 붙어 있는 경우, 예를 들어
fooZZZ
ZZZfoo
ZZZfooZZZ
이런 문자열 속의 foo 는 바꾸지 않습니다.



대소문자 구분없이 바꾸려면
:%s/foo/bar/i
이렇게 i 옵션을 붙입니다. 이것은 ".vimrc" 파일을 고치지 않아도 작동합니다.




전역 치환

이 경우
foofoofoofoofoofoofoofoo

이렇게
barfoofoofoofoofoofoofoo

문장의 첫번째 foo만 bar로 치환됩니다. 문장의 모든 foo를 bar로 치환하려면
:%s/foo/bar/g
이렇게 g 옵션을 사용합니다.


:%s/foo/bar/gi
이렇게 여러 옵션을 혼용할 수도 있습니다.




패턴을 이용한 찾기 바꾸기 응용
글쓴이 : 탐험가 (2001년 10월 06일 오후 04:09) 읽은수: 5,495 [ vi # 트랙백(0) 인쇄용 페이지 본문 E-Mail로 보내기 ]
vi 상황

자바에서 Vector를 이용하여 코딩하고 있었는데.. Vector는 불필요한 Synchronized를 위한 처리를 해서

오버헤드가 쩜 있을꺼 같아서.. 크기가 고정된 것은 String[](배열)로 바꾸려고 한다

vListData.get(1) --> vListData[1]로...1의 값은 숫자이고 여러 값으로 바뀔수 있음..

기존 해결책
검색해서....다 바꾼다..-_-aa

( 검색 한게 어딘가..-_-aa )

해결책
패턴 매칭으로..찾아본다..

:%s/vListData.get(([0123456789]*))/vListData[1]/g

끝~~

조금 설명하면.. 앞의 패턴에서 ( 과 ) 사이의 것을

대치할 패턴의 1에 넣는 것이 핵심이다.

두개 있으면 2하면 될까? 모르겠다...-_-aa

여튼..정확한 설명은 아니구요...대충 그렇지 않을까 싶은거니

확실하게 아는 분 있으면...답글 달아주세요..

혹..확인하며 바꾸고 싶으면..끝에 g대신 gc를 입력하면

검색한다음... 바꿀 부분을 알려준다...그때..y를 누르면

바꾸어준다...

혹 잘못된거 있으면 알려주시길..

Unix PowerTools란 책을 참고했습니다..

편안한 하루!

p.s SunOS에서 VI만 쓰는데 VI에서 Syntax Highlight쓸 수 없나요?

VIM을 깔면 보안상 문제는 안생기나요? -_-aaa

<  여러파일 확장자 바꾸기 | FTP계정만 주고 Shell계정은 주지 않기  >
패턴을 이용한 찾기 바꾸기 응용 | 답장: 2개(RSS) | 본문에 답장
정렬 : 
답장 무우 (2001년 12월 09일 오전 09:35)
[0123456789] 는 [0-9] 요로케...
2같은것도 다 먹는다는 거.. 실험해 보셔서 아셨져?

1 2 로 두개의 문자열 swap 을 할 수 있을 겁니다.
:%s/(앗싸)(좋구나)(108계단)/231/g
이렇게 하시면
'앗싸좋구나108계단' 이 '좋구나108계단앗싸' 로 바뀝니다.

이거 왕입니다. 엄청난 양의 데이터를 가진 문서에서
순서바꾸기를 할 때 정말끝내줍니다.

123 def ghi
432 ius dkf
089 cdo lks

이걸 숫자를 가운데로, 두번째는 끝, 세번째 처음으로 보내려면

:%s/([0-9]*) ([a-z]*) ([a-z]*)/3 2 1/g

제가 아는 한도에서는 이게 가장 깔끔 -_-/~

정규 표현식의 세계는 오묘한것 같아여 넘 흑 ㅠ.ㅠ/~
[ 이글에 답장 | 본문에 답장 | 책갈피 ]

답장 임동현 (2001년 10월 08일 오전 09:30)
vim 까세요..

vi 에서 느낄수 없는 수많은 기능이 존재합니다.

보안상 문제가 있을까요?.

거의 /tmp 이런 문제 빼곤 없을걸요?.

지금은 아마 다 패치 되지 않았나 쉽네요..

물론.. syntax on 도 되지요..



 문자열 바꾸기

:%s/ABC/DEF/g    global substitution
:%s/ABC/DEF/c    check 
 
# 내용수정

    * cw: 단어 바꾸기
    * r  : 한글자 바꾸기   R: 모두 바꾸기
    * ~ : 대소문자바꾸기


# : <범위>s/old/new/<옵션>

    * Ex)
          o :s/add/plus/g
                + 전체 문서에서 add 를 plus로 치환
          o :s//etc///etc/local//g
                + 현재 편집중인 전체 문서에서 /etc/ 를 /etc/local/ 로 치환

http://www.gentoo.org/doc/ko/vi-guide.xml

텍스트 치환

텍스트의 패턴을 치환하기 위해 우리는 ex 모드를 사용한다. 만약 여러분이 현재 줄에서 처음으로 나타나는 패턴을 치환하고 싶다면, :s/<regexp>/<replacement>/라고 입력하고 <ENTER>를 치라. 여기에서 <regexp>는 여러분이 찾고자 하는 패턴이고 <replacement>는 바꿀 문자열이다. 현재 줄에 있는 모든 패턴을 치환하고 싶다면 :s/<regexp>/<replacement>/g라고 입력하고 엔터를 치라. 파일 안에 있는 모든 패턴을 치환하고 싶다면, :%s/<regexp>/<replacement>/g라고 입력하라. (보통 여러분은 이것을 원할 것이다.) 만약 여러분이 파일 안에 있는 모든 패턴을 치환하고 싶지만, vi가 각각의 변경에 대해 여러분의 확인을 받도록 하고 싶다면 :%s/<regexp>/<replacement>/gc라고 입력하고 엔터를 치라. (g는"global"을, c는 "confirm"을 의미한다.)





http://mwultong.blogspot.com/2006/09/regex-numbers-only.html

[정규식] 숫자로만 이루어진 행 찾기/매치시키기 - 정규표현식 REGEX Numbers Only

문서 중에서, 숫자로만 이루어진 행만 찾는 정규식입니다. 0에서 9까지의 아라비아 숫자 외의 다른 글자가 섞여 있으면 찾지 않습니다.

울트라에디트/EmEditor 정규식

에디터의 "찾기" 대화상자에서, Use Regular Expressions 에 체크해 주어야 "정규식으로 찾기"가 작동합니다. (정규식이 아닌 일반 텍스트를 찾기 위해서는 이 옵션을 해제해야 합니다.)

숫자로만 이루어진 행 찾기는 다음과 같습니다.

^d+$


^ : 행의 첫부분
d : 0~9까지의 숫자
+ : 바로 앞의 글자 (여기서는 숫자)가 1개나 1개 이상 있음. (즉 숫자가 최소한 1개 있음)
$ : 행의 마지막 부분


Vim / Gvim 정규식

Vim(VI) 에디터에서는 다음과 같이 합니다.

/^d+$


/ : 이것은 정규식의 일부가 아니고 빔 에디터의 찾기 명령입니다.

또한, 이렇게 + 앞에 를 붙여야 합니다.


http://www.joinc.co.kr/modules.php?name=News&file=article&sid=49


2.3. ex 모드
2.3.1. 찾기/치환

vim 의 기능중 가장편리한 기능으리면 뭐니뭐니 해도, 정규표현식을 이용한 강력한 찾기기능과 치환기능이라고 할수 있을것이다. 물론 다른 대부분의 에디터들도 찾기기능과 치환기능을 제공하긴 하지만, vim 의 기능은 다른 에디터들에 비해서 정말로 독보적인 편리함과 강력함을 제공한다. vi 사용자가 다른 에디터로 넘어가기 힘든이유중 가장큰 이유가, 바로 "키를 이용한 방향입력" 과 "찾기 및 치환" 기능 때문이다.

사실 찾기 치환의 기능을 제대로 이해하고 사용하기 위해서는 정규표현식(regular expression) 에 대한 이해가 필요로 하는데, 이것은 다음의 사이트를 참조하기 바란다. 정규 표현식의 간략한 소개

먼저 찾기 기능에 대해서 알아보겠다. 찾기기능은 ':/패턴/' 를 이용 하면된다. 찾기 원하는 문자혹은 패턴을 입력하고 엔터키를 누르면 현재 커서위치에서 가장 가까운 곳에 위치한 문자열로 커서를 이동시킨다(문서 아래방향으로). 다음 문자열을 찾기를 원한다면 'n'키를 누르면 된다. 문서에서 가장 마지막에 이르르게 되면, 문서의 가장처음부터 다시 찾기 시작한다. 'Shift+n' 을 이력하면 반대 방향(문서의 위쪽으로)으로 찾기를 시작한다.

치환이야 말로 vim 의 꽃이라고 할수 있다. :[범위]s/[oldpattern]/[newpattern]/ 의 형식으로 사용하면 된다. 범위 지정은 visual block 을 이용할수도 있으며, 직접 범위를 입력할수도 있다. visual block 를 이용한 치환은 visual block 를 지정한다음 ':' 를 입력해서 ex 모드로 넘어가면 된다. 그리고나서 ':'<,'>s/[oldpattern]/[newpattern/' 과 같은 방법으로 치환하면 된다.

visual block 를 사용하지 않고 직접범위를 입력할수도 있다. :[시작],[마지막]s/[old]/[new]/ 식으로 범위를 지정하면 된다. 여기에는 몇가지 지정된 범위를 위한 특수 기호들이 있다. '%' 는 전체문서(처음부터 끝까지), '.' 은 현재, '$' 은 마지막 을 나타낸다. 숫자를 입력할경우 숫자는 라인을 나타낸다. 다음은 간단한 사용예이다.

# 문서 처음부터 마지막까지의 char 를 _char_ 로 치환한다.
:%s/char/_&_/g

# 현재(커서위치)부터 마지막까지의 char 를 _char_ 로 치환한다.
:.,$s/char/_&_/g

# buf_.*[255], buf_in[255], buf_get[255] 와 같은 문자열을 hello 로 변경한다. 
:1,10s/buf_.*[255]/hello/g
               

마지막에 쓰인 'g' 는 global 이다. 즉 해당 라인 전체에 걸쳐서 검색후 치환한다. 'g' 를 사용하지 않을경우 라인에서 처음에 검색된 문자만 치환하고 다음 라인으로 넘어간다.


http://ling.snu.ac.kr/hpshin/class/LangAndCom0402/lecture/regExp.htm

정규표현식 기초

저자 전정호 (mahajjh@myscan.org)

 

Copyright (c) 2001 Jeon, Jeongho.
Permission is granted to copy, distribute and/or modify this document
under the terms of the GNU Free Documentation License, Version 1.1
or any later version published by the Free Software Foundation;
with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.

 

이 글은 유닉스 사용과 관리에 필수인 정규표현식을 설명합니다. 또, 정규표현식을 처리하는 C 라이브러리도 마지막에 설명합니다.

vi 창 설정

VIM 분할(Window Split)

기본 분할

  1. 명령모드에서 분할을 할 수 있다.
  2. 분할하기 (수평분할)
    1. <Ctrl>+<w> 를 누른상태에서 <n> 키를 누른다.
       :^wn 
    2. <Ctrl>+<w> 를 누른상태에서 <s> 키를 누른다.
       :^ws 
  3. 이동하기
    1. <Ctrl>+<w>+<w> 키를 통해 이동을 한다.
       :^ww 
    2. <Ctrl>+<w>+<방향키> 키를 통해 이동을 한다.
       :^w[hjkl] 
  4. 현재 에서 다른 파일을 불러오기
    1. 명령모드에서 :e 불러올 파일이름
       :e /home/starlits/.vimrc 

확장 분할

  1. 명령모드(:) 에서 실행한다.
  2. 균등 수평분할
     :vs 
  3. 균등 수직분할
     :sp 
  4. 수치 수평분할 (왼쪽을 20만큼할당)
     :20vs 
  5. 수치 수직분할 (윗을 20만큼할당)
     :20sp 

파일관리

  1. 다른 파일을 분할된 에 불러오기
     :e <filename> vs 
  2. 분할을 이용하여 탐색기 열기( “./“는 현재폴더를 의미)
     :20vs./ 

분할 조정

  1. 수평분할일때 아래로 한줄 내리기 (한줄늘릴때마다 반복해야 하는 불편함이 있다)
     <Ctrl>+<w>+<+> 
  2. 수평분할일때 아래로 한줄 올리기 (한줄늘릴때마다 반복해야 하는 불편함이 있다)
     <Ctrl>+<w>+<-> 
  3. 수직분할일때 왼쪽으로 한줄 밀기 (한줄늘릴때마다 반복해야 하는 불편함이 있다)
     <Ctrl>+<w>+<>> 
  4. 수직분할일때 왼쪽으로 한줄 당기기 (한줄늘릴때마다 반복해야 하는 불편함이 있다)
     <Ctrl>+<w>+<<> 
  5. 현재 cursor가 위치한 닫기
     ^wc 
  6. 현재 cursor가 있는 을 최대화
     ^w_ 
  7. 분할 균등하게 조정하기
     ^w= 
  8. 현재 corsor가 있는 의 높이를 20줄로 하기 (동작하지 않음?)
     20^w_ 

2009년 11월 24일 화요일

vim 팁

vi 를 자유 자제로 사용 하기 위해서는 vi레지스터에 대해서 알 필요가 있습니다.
대부분의 사용자들이 vi 레지스터에는 관심이 없이 y & p 명령으로 복사 하기 붙여 넣기를
사용하는게 일반적인데.. windows환경에서는 ctrl + c & ctrl + v 만 알면 일반유저들은
클립보드 내부를 알필요가 없이 복사 붙여 넣기를 자유 자제로 사용 하지만..
vi에서는 단순 복사 붙여 넣기가 아닌 막강한 기능을 제공하고 있습니다..
그럼에도 우리가 너무 ctrl + c & ctrl + v에 길들여져 있기에 대부분 처음 vi를 접하면 복잡하고..
불편하기 그지 없는데.. vi 에 익숙해 질때까 되면 윈도우 메모장에서 그냥 작업 하는거랑
vi 에서 작업하는거랑 엄청난 차이가 있음을 새삼 느끼게 되죠..^^ 서문이 길었는데...
이제 본격적으로 vi 파워유저로 거듭나기 위한 몇가지 팁에 대해서 알아 보겠습니다

일단 소스 작성시 가장 많이 사용 돼는 복사 & 붙이기 ,검색, 정렬등을 보다 효육적으로 하기 위한
몇가지 팁에 대해 알아 보겠습니다

제목에서 알수 있듯이 vi를 접해본분들을 대상으로 효율적인 tip을 주제로 다룰것이므로..
기초적인 문서 작성 저장.. 이런 것은 생략 하겠습니다 (다들 아실테니까요)

복사 & 붙이기

복사 (잘라내기) 를 재대로 이해 하기 위해서는 vi의 내부 레지스터 이해가 필요 합니다
vi는 내부적으로 기본 17개의 레지스터를 가지고 있습니다  ( 1 ~ 9 이외 특수 목적으로 사용 )
(여기서 레지스터란.. 실제 레지스터가 아니라 레지스터 버퍼를 말하는것입니다)
일반적으로 짤라내기 dd 명령등을 사용하면 aaaa문장을 짤라 냈을 경우 레지스터 버퍼에
들어가게 되는데 ..

----------------------------------------------------------------
"1        "2         "3         "4        "5        "6        "7         "8         "9
aaa
----------------------------------------------------------------

----------------------------------------------------------------
"1        "2         "3         "4        "5        "6        "7         "8         "9
bbb      aaa
----------------------------------------------------------------

----------------------------------------------------------------
"1        "2         "3         "4        "5        "6        "7         "8         "9
ccc      bbb      aaa
----------------------------------------------------------------

----------------------------------------------------------------
"1        "2         "3         "4        "5        "6        "7         "8         "9
ddd      ccc      bbb       aaa
----------------------------------------------------------------

이와 같은 큐  형식 으로 들어 가게 됩니다

vi 17개 레지스터 버퍼 ( (:)ex모드에서  :reg로 레지스터버퍼 확인할수 있습니다 )

" "   "0   "1   "2   "3    "4    "5    "6    "7    "8    "9    "-   ".   "%    "#    "/
    ""  레지스터  ->  바로 이전에 지워진 내용이 항상 들어 갑니다
"1 ~ "9 레지스터  ->  지워진 내용이 큐 형식으로 들어 갑니다
    ".  레지스터  ->  최근까지 타이핑 한 내용이 들어 갑니다
   "%  레지스터 ->  현재 편집하는 파일명이 들어 갑니다
    "/  레지스터 ->  가장 최근에 검색한 문자열이 들어 갑니다

레지스터에 들어 있는 내용을 p명령으로 붙여 넣기 할수 있습니다
p명령은 가장 마지막에 복사 & 잘라내기 한 ( " " 레지스터에 있는 내용) 이 붙여 넣기가 됩니다
p명령으로 붙여넣기를 할때 특정 레지스터를 지정 할수도 있습니다
  -> "2p 와 같이 하게 되면 2번 레지스터에 있는 내용이 붙여 넣기가 됩니다 (명령모드 에서)
이처럼 vi레지스터를 이해한다면 복사하기 붙이기를 좀더 효율적으로 사용 할수 있습니다

반복되는 문자열 저장해서 쓰기

vi에서는 기본 17개 레지스터 이외 26개의(a ~ z)레지스터(버퍼)를 지원 합니다
사용 방법으로는 복사하고자 하는 라인의 맨앞에 커서를 위치한후
ex) "a3yy ( a레지스터에 현제 위치에서 3줄 복사하여 저장) 복사하기
붙여 넣기 -> "ap 로 붙여 넣기 하실수 있습니다

메크로 사용하기
vi에서 메크로를 사용한다는게 언듯 무슨 말인가 싶기도 한데.. 간략히 설명한다면..
똑같은 패턴의 작업을 레지스터에 입력 해 두었다가 그걸 사용하는 아주 편한 방법입니다
첨에 보시면 손이 많이 가는 만큼이나 복잡하게 느껴지는데 익숙해 진다면..
아주 편하게 사용 하실수 있습니다

우선 사용방법으로는 명령 모드에서 q를 누르면 밑에 하단에 기록중 이라고 표시가 됩니다
q를 누른후 부터 누르는 키를 모두 기록하여 레지스터에 집어 넣는데.. 그르므로 필요없는 키를
누르면 잡다한 작업을 하게 만드는 결과가 생기므로 꼭 필요한 작업만 추가 한후 기록을 끝낼때는
다시 q를 눌러서 기록 모드를 종료 해야 합니다.

간단한 예제로 주석을 다는 작업을 할경우

리눅스에서 /* */  주석이 먹히지 않으므로 c표준 // 주석만 먹히는데 그래서 여러 라인 주석을
경우 상당히 귀찮아 지는데.. 이경우 사용하면 효과 적입니다..

명령모드에서 q레지스터 이름(a ~ z)를 누른후

a레지스터에 기록 할경우 qa 라고 누른후 (패턴을) 기록을 하시면 됩니다 기록중이라고 표시 돼었을때  i키를 눌러서 입력 모드로 바꾼후 //입력을 한후 스페이스바로 한칸 띄운후 ESC 키를 누른후 입력 모드를 종료 시키고 (그래도 기록모드는 계속 유지되고 있음 q를 다시 누를 때까지..) 다음 라인으로 가기 위해 enter키를 누른후 q를 다시 눌러 기록 모드를 종료 합니다


입력후 레지스터를 확인 하려면 ex모드에서 : reg 명령을 통해서 보실수 있습니다.
(ex모드 -> : 을 눌러서 실행명령을 입력 할수 있는 모드를 얘기 합니다)
그럼 a레지스터에 기록한 작업이 특수문자로 들어가있는데 그걸 사용 하려면..
적용 시킬 라인수 @ 레지스터 이름 으로 사용 하실수 있습니다
ex ) 5@a  (a레지스터에 있는 작업을 커서 위치로 부터 밑으로 5라인에 적용 합니다 )


블록 지정

블록 지정후 많은 작업을 할수 있는데 그것을 알아 보겠습니다.
일단 불록 지정하는 방법에는 2가지가 있습니다
  -> ctrl + v 로 지정하는 방법 (라인 지정이 아닌 범위 지정 특정 범위를 포함 할때 사용)
  -> shift + v 로 지정 하는 방법 (라인 지정 단위)
지정하는 차이를 모르겠다 싶은 분들은 직접 해보시면 바로 아실수 있어요..
지정후 많은 직업이 있는데 대표적인것만 다루 겠습니다

~  -> 대소문자 반전
u  -> 소문자로
U -> 대문자로
y  -> 복사하기
d  ->  삭제하기

>  ->  들여쓰기 ( visual c++ => Tab )
<  -> 내어쓰기  ( visual c++ => Shift + Tab )
=  -> 자동정렬  ( visual c++ => Alt+ F8 )

특히 들여 쓰기, 내어 쓰기, 자동 정렬 기능이 강력하지만 모르는 분들이 의외로 많은 기능..

검색 하기

검색하기는 / 나 ? 로 사용 하는게 일반적인데... 그외 {} 괄호 짝찾기 ,
같은 단어 찾기 에대해서 알아 보겠습니다
일반적인 검색하기이외  for,if 문이 중첩되어 소스가 복잡해질때 빠진 괄호 찾기가 쉽지가 않은데
그때 사용 하는 방법이 짝을 찾고자 하는 {} 괄호에 커서를 위치 시킨후 명령모드에서 % 를 입력
하면 가장 가까운 괄호 짝을 찾아 갑니다

(visual c++ 에선 crtl + ] 로 괄호 찾기가 가능하지만 vi에서는 ctrl + ] 키는 ctags 가 설정
되어 있을경우 tags를 찾아 감으로 주의가 필요 합니다 )

같은 단어 찾기

-> 찾고자 하는 단어에 커서를 위치 시킨후 (단어내 어디든 무관) 명령모드에서 * 입력
같은 단어가 찾아 집니다  일반 /검색후 찾아 가듯이 n 다음 단어 ctrl + n 뒤로 가실수 있습니다


정렬 하기

위에 블록 지정으로 정렬 하기가 강력해서 그것만으로도 충분하지만
전체 정렬하기 등 몇가지 tip을 살펴 보겠습니다

들여쓰기 , 내어쓰기

라인수 >>, << 입력으로 들여쓰기 및 내어 쓰기를 할수 있습니다


정렬하기

함수 시작 { 에 커서를 위치 시킨후 명령모드(ESC누른상태) 에서 =100 을 누른후 방향키를 이동
해보시면 정렬이 되는걸 확인 할수 있습니다 특히 마우스 오른쪽을 눌러서 복사를 하게 되면
자동으로 들여쓰기가 되면서 소스가 파도타기 처럼 엉망이 되기 쉬운데 이때 정말 효율적이죠
=100 은 { } 범위 안에서만 정렬이 됩니다
파일 전체에 범위를 주려면 명령모드에서 gg=G  입력후 방향키를 움직이면 정렬이 됩니다


둘다 명령모드에서 입력후 방향키를 입력 안하면 정렬이 됐지만 변경된거 확인 할수가 없는데.
방향키를 눌러서 커서를 이동 시키면 그때 실제 정렬이 이루어 집니다

마킹으로 이동하기

마킹으로 이동하는 방법은 주로 사용 하지는 않지만 vi가 지원하는 기능이므로 알아 보겠습니다
마킹에는 크게 3가지 종류가 있지만 주로 2가지만 사용 합니다

전역 마킹 (A ~ Z) -> 현재 파일을 포함한 다른 파일에서도 사용 가능 합니다
지역 마킹 (a ~ z) -> 현재 파일에서만 마킹이 가능 합니다

사용 형식

m[마킹 알파벳 (전역 대문자, 지역 소문자)] - 마킹하기

'[이동할 마킹 알파벳] -> 마킹된 위치로 이동하기

마킹하기는 사용하는 유저에 성향에 따라 편할수도 불현할수도 있습니다
저 같은 경우 불현해서 잘 쓰지 않지만.. 편해서 쓰는 분도 있을테니까요..^^
출처 - 한빛미디어 - 유닉스 리눅스 프로그래밍 필수 유틸리티, 저자 -백창우-
책에 있는 내용지지만 급하게 볼때 책보단 정리된 파일이 편함으로 정리 삼아 만들어 봤습니다
안돼는게 있으면 댓글 달아주세요..

출처 : Tong - mapoo님의 vi통

2009년 11월 23일 월요일

vim 단축키

명령모드 이동키
설명 설명
h 왼쪽으로 이동 l 오른쪽으로 이동
j 아래로 이동 k 위로 이동
w/W 다음 단어의 첫글자로 이동 b/B 이전 단어의 첫 글자로 이동
e/E 단어의 마지막 글자로 이동 <CR> 다음 행의 첫 글자로 이동
^ 행의 첫 글자로 이동 $ 행의 마지막 글자로 이동
+ 다음 행의 첫 글자로 이동 - 이전 행의 첫 글자로 이동
( 이전 문장의 첫 글자로 이동 ) 다음 문장의 첫 글자로 이동
{ 이전 문단으로 이동 } 다음 문단으로 이동
H 커서를 화면의 맨 위로 이동 z<CR> 현재 행을 화면의 맨 위로 이동
M 커서를 화면의 중앙으로 이동 z. 현재 행을 화면의 중앙으로 이동
L 커서를 화면 최하단으로 이동 z- 현재 행을 화면의 최하단으로 이동
[n]H 커서를 위에서 n행으로 이동 [n]L 커서를 아래에서 n행으로 이동
CTRL+u 반 화면 위로 스크롤 CTRL+d 반 화면 아래로 스크롤
CTRL+b 한 화면 위로 스크롤 CTRL+f 한 화면 아래로 스크롤
gg, 1G 문서의 맨 첫 행으로 이동 G 문서의 맨 마지막 행으로 이동
[n]G n행으로 이동 :[n] n행으로 이동

편집키
설명 설명
x,dl 커서 위치의 글자 삭제 x,dh 커서 바로 앞의 글자 삭제
dw 한 단어를 삭제 dd 커서가 있는 행을 삭제
d0 커서 위치로부터 행의 처음까지 삭제 D,d$ 커서 위치부터 행의 끝까지 삭제
dj 커서가 있는 행과 그 다음 행을 삭제 dk 커서가 있는 행과 그 앞행을 삭제
~ 대소문자 전환 d 삭제
yw 현재 커서의 한단어 복사 yy 한줄 복사
y 복사 p 붙여넣기
> 행 앞에 탭 삽입 < 행 앞에 탭 제거
: 선택된 영역에 대하여 ex 명령 J 행을 합침
U 대문자로 만듦 u 소문자로 만듦
검색 /[문자열] 또는 ?[문자열] 검색 다음에 매칭 n, 이전에 매칭 N
치환 :[범위]/[매칭]/[치환]/[행 범위] => :%s/old/new/g

다중 창 사용하기
설명 설명
CTRL+wn 새창 열기(수평) CTRL+wv 새창 열기(수직)
CTRL+ws 현재 파일을 수평으로 나눔 CTRL+w^ 수평으로 나누고, 이전 파일 열기
CTRL+wf 창을 수평으로 나눔, 커서위치 파일 열기 CTRL+wi 커서위치의 단어가 정의된 파일 열기
CTRL+wq 현재 커서의 창 종료 CTRL+wc 현재 커서의 창 닫기
CTRL+wo 현재 커서의 창만 남기고 모드 닫기    
CTRL+wh 왼쪽 창으로 이동 CTRL+wj 아래 창으로 이동
CTRL+wk 위쪽 창으로 이동 CTRL+wl 오른쪽 창으로 이동
CTRL+ww 창을 순차적으로 CTRL+wp 최근 이동 방향으로 이동
CTRL+wt 최상위 창으로 CTRL+wb 최하위 창으로
CTRL+wr 순차적으로 창의 위치 순환 CTRL+wx 이전 창과 위치 바꿈
CTRL+w= 창의 크기를 모두 균등하게 함 CTRL+w_ 수평 분활에서 창 크기 최대화
CTRL+w| 수직 분활에서 창의 크기 최대화 CTRL+w[N]+ 창의 크기를 N행 만큼 증가
CTRL+w[N]- 창의 크기를 N행 만큼 감소 CTRL+w[N]> 창의 크기를 오른쪽으로 N행 증가
CTRL+w[N]< 창의 크기를 왼쪽으로 N행 증가    

/etc/vim/vimrc

runtime! debian.vim

if has("syntax")
  syntax on
endif


if filereadable("/etc/vim/vimrc.local")
  source /etc/vim/vimrc.local
endif


syn on             "    문법 강조기능을 사용한다.
set nu            "      숫자
set autoindent    "   자동으로 들여쓰기를 한다.
set cindent       "     C 프로그래밍을 할때 자동으로 들여쓰기를 한다.
set smartindent  "   좀더 똑똑한 들여쓰기를 위한 옵션이다.
"set textwidth=79     만약 79번째 글자를 넘어가면
"set wrap               자동으로 를 삽입하여 다음 줄로 넘어간다.
set nowrapscan    "  검색할 때 문서의 끝에서 다시 처음으로 돌아가지 않는다.
set nobackup      "   백업 파일을 만들지 않는다.
set visualbell       "  키를 잘못눌렀을 때 삑 소리를 내는 대신 번쩍이게 한다.
set ruler            "     화면 우측 하단에 현재 커서의 위치 표시
set tabstop=4        "   Tab 크기를 4로 설정
set shiftwidth=4     "  자동들여쓰기 크기를 4로 설정
set history=999      "  이전 작업하던 라인을 기억한다.
set sc               "      완성중인 명령 표시

set sts=0           "    탭 -> 공백 변환 기능 (사용안함)
set wrap           "    자동 줄바꿈 안함
set magic         "    매직 기능 사용

set sol                "      여러가지 동작시 줄의 시작으로 자동 이동
set mps+=<:>          "  괄호 짝 <> 추가
set sm               "       추가된 괄호짝 보여주는 기능
set background=light "   구문강조 기능
set paste!        " 붙여 넣기시 계단현상 제거


" 검색 기능 설정

set hls           "      검색어 강조기능
set nows          "   검색시 파일끝에서 처음으로 되돌리기 안함
set ic            "      검색시 대소문자 구별안함
set scs           "    똑똑한 대소문자 구별기능 사용

set nocompatible       " Use Vim defaults (much better!)
set bs=2                "     allow backspacing over everything in insert mode
set pastetoggle=<Ins>  " 자동들여쓰기 기능 (Paste 시에는 Off 하는게 좋다)
set foldmethod=marker " 폴더기능 사용
set fileencoding=utf-8 " UTF-8을 기본 저장포맷으로
set encoding=utf-8     " UTF-8을 기본 읽기포맷으로
"set termencoding=euc-kr " 단 터미널은 euc-kr로
" set linebreak         Don't wrap words by default
set viminfo='20,"50   "  read/write a .viminfo file, don't store more than 50 lines of registers
filet on              "  파일 종류 자동인식

set nowrap     "   한화면을 넘는 줄을 줄바꿈하지 않는다.

" 폴드 옵션
"set foldmethod=marker   폴더기능 사용

"map <F1> :w<CR>                    F1키를 터미널에서 먹어서 안됨
map <F2> :35vs ./<CR>:set nonu<CR>
map <F3> O^Ww                
map <F4> v%zf              
map <F5> zo               
map <F6> [i              
map <F7> :set nonu<CR>  
map <F8> :set nu<CR>   
map <F9> K            
map <F10> :! make<CR>
map <F11> :w<CR>    
map <F12> ^t       
 

2009년 10월 30일 금요일

윈도우7 vmware 네트워크 설정 NAT

Windows 7  이 Host OS 이고 VMware에서 기타 OS가 Guest 이면 정상적인 방법으로 NAT를 사용하실 수는 없습니다.

Windows 7 기능 중 ICS(Internet Connection Sharing) 기능을 이용하는 방법입니다.

제가 build 7068(?)인걸로 알고 있는데 이것을 이용하면 Virtual Network이 192.168.137.XXX으로 설정되게 됩니다.


1. Run the Virtual Network Editor as Administrator (Use RunAS or right click and Run as Administrator on vmnetcfg.exe)

-- Virtual Network Editor를 Administrator 모드로 시작합니다. (vmnetcfg.exe를 마우스 오른쪽 클릭하시고 Run as Administrator 를 클릭하세요)


2. Goto Host Virtual Adapters and remove all VMNet instances (VMNet1 and VMNet8 typically).

-- Host Virtual Adapters를 클릭하고 VMNet 으로 등록된 모든 어뎁터를 지웁니다. (보통 VMNet1 과 VMNet8 )


3. Click Apply.

--  적용(Apply) 버튼을 누릅니다.


4. Add New and Assign it the new adapter to VMnet1.

-- Add New 버튼을 누르고 Assign 버튼을 눌러서 VMNet1을 새로운 어뎁터로 설정합니다.


5. Click Apply.

-- 적용(Apply) 버튼을 누릅니다.


6. Select the Host Virtual Networking tab.

-- Host Virtual Networking 탭을 누르세요.


7. Click the > next to VMnet1 and change the address and subnet to the ICS network (192.168.0.0 / 255.255.255.0)

--  VMNet1 옆에 있는 ">" 버튼을 누른다음 subnet IP를 192.168.137.0/255.255.255.0 로 설정합니다.

역주) 192.168.0.0 은 Beta Version 중 build 7000 이하로 알고 있습니다. 제 버전은 7068(?) 인가 인데 192.168.137.0 을 ICS에서 사용하고 있습니다.

확인 하는 방법은

Control Panel\Network and Internet\Network and Sharing Center 로 이동하시고

Local Area Connection 을 클릭 -> Properties를 클릭 -> Sharing 탭 클릭 하신 후 첫번째 항목 클릭하시고

Network Adaptor를 하나 클릭하시면 경고 문구가 뜨면서 해당 IP 대역을 사용해야 한다고 알려줍니다.


8. Click Apply.

-- 적용(Apply) 버튼을 누릅니다.


9. Go to the NAT tab and select VMNet1.

-- NAT tab을 누르시고 VMNet1을 선택합니다.


10. Click Edit and change the Gateway to the ICS gateway IP (192.168.0.1)

-- Edit를 클릭하시고 Gateway IP를 192.168.137.1 로 변경합니다.

역주) VMWare는 Gateway IP를 192.168.XXX.2로 사용하는 것을 권고하지만 Windows의 ICS 를 이용하려면 192.168.XXX.1로 하여야 합니다.


11. Click Apply and restart the NAT service. (Counter-intuitive, I know.)

-- NAT 서비스 Restart 버튼을 누르고 적용버튼을 누릅니다.


12. Go to the DHCP tab.

-- DHCP 탭으로 이동합니다.


13. Add VMNet1 and remove all others.

-- VMNet1 을 추가하고 나머지는 모두 지웁니다.


14. Click Apply.

-- 적용 버튼을 누릅니다.


15. Select Properties of VMNet1 in DHCP Tab.

-- DHCP 탭 내에서 VMNet1의 properties 를 선택합니다.


16. Enter a Start and End Address for DHCP Scope (192.168.0.50 to 192.168.0.75 as an example)

-- DHCP 할당 IP 대역을 Start와 End Address에 각각 입력합니다. (기본 값으로 두셔도 상관 없습니다.)


17. Adjust client lease to a few days to avoid potential DHCP client renewal timeouts.

-- Renewal time을 되도록 길게 해서 DHCP IP가 Release되지 않도록 합니다. (저의 경우 한달로 해놨습니다.)


18. Click OK / Click Apply.

-- OK 버튼을 누르고 Apply 버튼을 누릅니다.


19. Select Host Only Networking for every VM that needs NAT out to your host network/internet.

-- VMWare의 모든 Guest OS의 Network Setting을 Host Only Networking으로 변경합니다.

역주) 물론 Guest OS는 DHCP 모드 로 되어야 하겠죠. Static IP도 사용 가능 할 것 으로 보입니다 만 실험해보진 않았습니다.


20. Enable ICS (Internet Connection Sharing) on the W7 Host network card that provides connectivity. Select VMNet1 as the network card that needs access.
-- Windows 7 에서 (7) 번에서 설명드린 것처럼 ICS를 활성화 하시고 Network Adopter를 VMNet1을 선택하시면 모든 셋팅이 끝납니다.

2009년 10월 29일 목요일

C/C++ volatile 키워드


2006년 9월 마이크로소프트웨어 기고글입니다.


약 60여개의 C++ 키워드 중에 가장 사용 빈도가 낮은 키워드는 무엇일까? 정답은 volatile이다. 대부분의 C/C++ 참고 서적은 1-2줄 정도로 volatile이 컴파일러의 최적화(optimization) 막아준다고만 설명하고 있다. 또한 Java5 이후로 명확한 메모리 모델이 확립된 자바와는 달리 C/C++의 경우 volatile에 대한 명확한 표준이 없고 컴파일러마다 구현에 차이가 있다는 점도 volatile 키워드의 사용을 어렵게 하고 있다. 하지만 임베디드 시스템이나 멀티쓰레드 프로그래밍이 보편화된 만큼, 이 글에서는 volatile 키워드의 기초부터 다시 살펴보고자 한다.


volatile 소개

volatile로 선언된 변수는 외부적인 요인으로 그 값이 언제든지 바뀔 수 있음을 뜻한다. 따라서 컴파일러는 volatile 선언된 변수에 대해서는 최적화를 수행하지 않는다. volatile 변수를 참조할 경우 레지스터에 로드된 값을 사용하지 않고 매번 메모리를 참조한다. 왜 volatile이라는 키워드가 필요한지 이해하려면 먼저 일반적인 C/C++ 컴파일러가 어떤 종류의 최적화를 수행하는지 알아야 한다. 가상의 하드웨어를 제어하기 위한 다음 코드를 살펴보자.


*(unsigned int *)0x8C0F = 0x8001

*(unsigned int *)0x8C0F = 0x8002;

*(unsigned int *)0x8C0F = 0x8003;

*(unsigned int *)0x8C0F = 0x8004;

*(unsigned int *)0x8C0F = 0x8005;

<!--[if !supportEmptyParas]--> 잘못된 하드웨어 제어 코드

<!--[endif]-->


이 코드를 보면 5번의 메모리 쓰기가 모두 같은 주소인 0x8C0F에 이루어짐을 알 수 있다. 따라서 이 코드를 수행하고 나면 마지막으로 쓴 값인 0x8005만 해당 주소에 남아있을 것이다. 영리한 컴파일러라면 속도를 향상시키기 위해서 최종적으로 불필요한 메모리 쓰기를 제거하고 마지막 쓰기만 수행할 것이다. 일반적인 코드라면 이런 최적화를 통해 수행 속도 면에서 이득을 보게 된다.

하지만 이 코드가 MMIO(Memmory-mapped I/O)처럼 메모리 주소에 연결된 하드웨어 레지스터에 값을 쓰는 프로그램이라면 이야기가 달라진다. 각각의 쓰기가 하드웨어에 특정 명령을 전달하는 것이므로, 주소가 같다는 이유만으로 중복되는 쓰기 명령을 없애버리면 하드웨어가 오동작하게 될 것이다. 이런 경우 유용하게 사용할 수 있는 키워드가 volatile이다. 변수를 volatile 타입으로 지정하면 앞서 설명한 최적화를 수행하지 않고 모든 메모리 쓰기를 지정한 대로 수행한다.


*(volatile unsigned int *)0x8C0F = 0x8001

*(volatile unsigned int *)0x8C0F = 0x8002;

*(volatile unsigned int *)0x8C0F = 0x8003;

*(volatile unsigned int *)0x8C0F = 0x8004;

*(volatile unsigned int *)0x8C0F = 0x8005;

올바른 하드웨어 제어 코드


특정 메모리 주소에서 하드웨어 레지스터 값을 읽어오는 프로그램의 경우도 마찬가지다. 아래 코드의 경우 같은 주소에서 반복적으로 메모리를 읽으므로, 최적화 컴파일러라면 buf[i] = *p;에서 *p를 한 번만 읽어온 후에 그 값을 반복해 사용할 것이다. 하지만 volatile 키워드가 있다면 *p를 참조할 때마다 직접 메모리에서 새 값을 가져와야 한다. 이 경우는 하드웨어가 메모리 0x8C0F 번지 값을 새롭게 변경해 주는 경우이다.


void foo(char *buf, int size)

{

     int i;

     volatile char *p = (volatile char *)0x8C0F;

<!--[if !supportEmptyParas]--> <!--[endif]-->

     for (i = 0 ; i < size; i++)

     {

         buf[i] = *p;

         ...

     }

}

하드웨어 레지스터 읽기


가시성


volatile 키워드는 앞서 살펴본 하드웨어 제어를 포함하여 크게 3가지 경우에 흔히 사용된다.

<!--[if !supportEmptyParas]--> <!--[endif]-->

(1) MMIO(Memory-mapped I/O)

(2) 인터럽트 서비스 루틴(Interrupt Service Routine)의 사용

(3) 멀티 쓰레드 환경

<!--[if !supportEmptyParas]--> <!--[endif]-->

세 가지 모두 공통점은 현재 프로그램의 수행 흐름과 상관없이 외부 요인이 변수 값을 변경할 수 있다는 점이다. 인터럽트 서비스 루틴이나 멀티 쓰레드 프로그램의 경우 일반적으로 스택에 할당하는 지역 변수는 공유하지 않으므로, 서로 공유되는 전역 변수의 경우에만 필요에 따라 volatile을 사용하면 된다.


int done = FALSE;

<!--[if !supportEmptyParas]--> <!--[endif]-->

void main()

{

     ...

     while (!done)

     {

         // Wait

     }

     ...

}

<!--[if !supportEmptyParas]--> <!--[endif]-->

interrupt void serial_isr(void)

{

     ...

     if (ETX == rxChar)

     {

         done = TRUE;

     }

     ...

}

serial.c


위 시리얼 통신 예제는 전역 변수로 done을 선언해서 시리얼 통신 종료를 알리는 ETX 문자를 받으면 main 프로그램을 종료시킨다. 문제는 done이 volatile이 아니므로 main 프로그램은 while(!done)을 수행할 때 매번 메모리에서 done을 새로 읽어오지 않는다는 점이다. 따라서 serial_isr() 루틴이 done 플래그를 수정하더라도 main은 이를 모른 채 계속 루프를 돌고 있을 수 있다. done을 volatile로 선언해주면 매번 메모리에서 변수 값을 새로 읽어오므로 이 문제가 해결된다.

인터럽트의 경우와 마찬가지로 멀티 쓰레드 프로그램도 수행 도중에 다른 쓰레드가 전역 변수 값을 임의로 변경할 수 있다. 하지만 컴파일러가 코드를 생성할 때는 다른 쓰레드의 존재 여부를 모르므로 변수 값이 변경되지 않았다면 매번 새롭게 메모리에서 값을 읽어오지 않는다. 따라서 여러 쓰레드가 공유하는 전역 변수라면 volatile로 선언해주거나 명시적으로 락(lock)을 잡아야 한다.

이처럼 레지스터를 재사용하지 않고 반드시 메모리를 참조할 경우 가시성(visibility) 이 보장된다고 말한다. 멀티쓰레드 프로그램이라면 한 쓰레드가 메모리에 쓴 내용이 다른 쓰레드에 보인다는 것을 의미한다.


문법과 타입


C/C++의 volatile은 상수(constant)를 선언하는 const와 마찬가지로 타입에 특정 속성을 더해주는 타입 한정자(type qualifier)이다. const int a = 5; 라고 선언했을 경우 a라는 변수는 정수 타입의 변수이면서 동시에 상수의 속성을 가짐을 의미한다. 같은 방식으로 volatile int a; 라고 선언해주면 정수 변수 a는 volatile 속성을 가지게 된다.

조심해야 할 것은 포인터 타입에 volatile을 선언하는 경우이다. int *를 volatile로 선언하는 몇 가지 방법을 비교해보자.


volatile int* foo;

int volatile *foo;

int * volatile foo;

int volatile * volatile foo;

volatile의 선언


복잡하게 보이지만 원리는 const 타입 한정자와 동일하다. *를 기준으로 왼쪽에 volatile 키워드가 올 경우 포인터가 가리키는 대상이 volatile함을 의미하고, * 오른쪽에 volatile 키워드가 올 경우에는 포인터 값 자체가 volatile임을 의미한다. volatile이 *의 양쪽에 다 올 경우는 포인터와 포인터가 지시하는 대상이 모두 volatile함을 의미한다. 하드웨어 제어의 예처럼 일반적으로 포인터가 가리키는 대상이 volatile 해야 할 경우가 많으므로 volatile int * 형태가 가장 많이 사용된다. volatile int*와 int volatile*은 동일한 의미이다.


01: int foo(int& a)

02: {

03: }

04:

05: int bar(volatile int& b)

06: {

07: }

08:

09: int main()

10: {

11:     volatile int a = 0;

12:     int b = 0;

13:

14:     foo(a);

15:     bar(b);

16: }

<!--[if !supportEmptyParas]--> <!--[endif]-->

$ g++ a.cc

a.cc: In function `int main()':

a.cc:14: error: invalid initialization of reference of type 'int&' from expression of type 'volatile int'

a.cc:2: error: in passing argument 1 of `int foo(int&)'


또한 int는 volatile int의 서브타입에 해당한다. 서브 타입을 쉽게 설명하면 A를 요구하는 곳에 언제든지 B를 사용할 수 있다면 B는 A의 서브 타입이다. 쉬운 예로 Class의 경우 Derived 클래스는 Base 클래스의 서브 타입이다. volatile int를 요구하는 곳에 언제든지 int를 넘길 수 있으므로 int는 volatile int의 서브 타입이라고 말할 수 있는 것이다. 그 예로 위 C++ 프로그램은 컴파일 에러가 난다. volatile int를 받는 bar() 함수에 int 인자로 호출하는 것은 아무 문제없지만, int를 요구하는 foo() 함수에 volatile int 타입인 a를 넘기면 컴파일 에러가 나는 것이다.


01: int foo(int& a)

02: {

03: }

04:

05: int bar(const int& b)

06: {

07: }

08:

09: int main()

10: {

11:     const int a = 0;

12:      int b = 0;

13:

14:     foo(a);

15:      bar(b);

16: }

foobar2.cc


이 관계가 쉽게 이해되지 않는다면 위의 예처럼 volatile 키워드를 같은 타입 한정자인 const로 대체해보자. 변수 a는 const int 타입이므로 이미 상수로 선언되었다. const int인 a를 int를 요구하는 foO() 함수에 넘기면 const라는 가정이 깨어지므로 컴파일 에러가 된다. 반대로 b의 경우 원래 const가 아니었지만 bar로 넘기면서 const 속성을 새롭게 부여받게 된다. const 속성을 부여받는다고 말하면 무엇인가 기능이 추가되는 것 같지만, 이는 바꿔 말해서 원래 int의 2가지 기능인 읽기, 쓰기에서 쓰기 기능이 사라지는 것으로 볼 수도 있다. 이를 클래스로 표현해 보면 다음과 같을 것이다.


class ConstInteger {

public:

     ConstInteger(int v) : value(v) {}

     int get() { return value; }

<!--[if !supportEmptyParas]--> <!--[endif]-->

protected:

     int value;

};

<!--[if !supportEmptyParas]--> <!--[endif]-->

class Integer : public ConstInteger {

public:

     Integer(int v) : ConstInteger(v) {}

     void set(int v) { value = v; }

};

ConstInteger.cc


위 클래스를 두고 보면 volatile/const int와 int의 관계가 명확해진다. Integer는 ConstInteger을 상속한 클래스이므로 ConstInteger를 요구하는 곳 어디에나 쓸 수 있는 것이다. 반대로 Integer가 필요한 곳에 ConstIntger를 넘기면 set() 메쏘드가 없으므로 문제가 된다. 따라서 컴파일러는 이를 금지하는 것이다.

volatile의 const와 같은 맥락에서 생각할 수 있다. volatile 속성을 부여받는 다는 것은 바꿔 말하면 컴파일러가 최적화를 할 자유를 잃는다고 말할 수 있다. ConstInteger의 경우만큼 명확하지는 않지만 이 관계를 클래스로 생각해 본다면 아마 다음과 같을 것이다.


class VolatileInteger {

public:

     VolatileInteger(int v) : value(v) {}

     int get() { return value; }

     void set(int v) { value = v; }

<!--[if !supportEmptyParas]--> <!--[endif]-->

protected:

     int value;

};

<!--[if !supportEmptyParas]--> <!--[endif]-->

class Integer : public VolatileInteger {

public:

     Integer(int v) : VolatileInteger(v) {}

     void optimize();

};

VolatileInteger.cc


재배치(reordering)


지금까지 volatile 키워드의 일반적인 기능과 문법에 대해서 살펴보았다. C 표준은 volatile 키워드와 메모리 모델에 대한 명확한 정의를 내리지 않고 있기 때문에 컴파일러마다 그 구현에 다소 차이가 있다. C++ 표준은 volatile 대해 별도의 정의하지 않고 가능한 한 C 표준을 따르라고만 하고 있다.

마이크로소프트의 Visual C++를 예로 들어보면, volatile 키워드에 앞서 살펴본 가시성(visibility) 뿐만 아니라 재배치(reordering) 문제에 대한 해결책도 추가하였다. Visual C++의 volatile 변수는 다음과 같은 기능을 추가로 한다.

<!--[if !supportEmptyParas]--> <!--[endif]-->

(1) volatile write: volatile 변수에 쓰기를 수행할 경우, 프로그램 바이너리 상 해당 쓰기보다 앞선 메모리 접근은 모두 먼저 처리되어야 한다.

(2) volatile read: volatile 변수에 읽기를 수행할 경우, 프로그램 바이너리 상 해당 읽기보다 나중에 오는 메모리 접근은 모두 이후에 처리되어야 한다.

<!--[if !supportEmptyParas]--> <!--[endif]-->

재배치(reordering)는 컴파일러가 메모리 접근 속도 향상, 파이프라인(pipeline) 활용 등 최적화를 목적으로 제한된 범위 내에서 프로그램 명령의 위치를 바꾸는 것을 말한다. 우리가 프로그램에 a = 1; b = 1; c = 1; 이라고 지정했다고 해서 컴파일된 바이너리가 반드시 a, b, c 순서로 메모리를 쓰지 않을 수 있다는 뜻이다. 만약 a, c가 같은 캐시(cache)에 있거나 인접해 있어서 같이 쓸 경우 속도 향상을 볼 수 있다면 a = 1; c = 1; b = 1; 로 순서가 바뀔 수 있는 것이다.

Visual C++의 경우 volatile을 사용하면 컴파일러가 수행하는 이러한 재배치에 제약을 주게 된다. a = 1; b = 1; c = 1;에서 c가 volatile로 선언된 변수였다면 a = 1;과 b=1;은 반드시 c에 1을 대입하기 전에 일어나야 한다. 물론 a와 b 사이에는 순서가 없으므로 b = 1; a = 1; c = 1; 과 같은 형태로 재배치가 일어날 수는 있다. 재배치가 일어나지 않도록 보장하는 문제가 왜 중요한지는 MSDN에서 발췌한 다음 예를 통해 살펴보자.


#include <iostream>

#include <windows.h>

using namespace std;

<!--[if !supportEmptyParas]--> <!--[endif]-->

volatile bool Sentinel = true;

int CriticalData = 0;

<!--[if !supportEmptyParas]--> <!--[endif]-->

unsigned ThreadFunc1( void* pArguments ) {

while (Sentinel)

Sleep(0); // volatile spin lock

<!--[if !supportEmptyParas]--> <!--[endif]-->

// CriticalData load guaranteed after every load of Sentinel

cout << "Critical Data = " << CriticalData << endl;

return 0;

}

<!--[if !supportEmptyParas]--> <!--[endif]-->

unsigned ThreadFunc2( void* pArguments ) {

Sleep(2000);

CriticalData++; // guaranteed to occur before write to Sentinel

Sentinel = false; // exit critical section

return 0;

}

<!--[if !supportEmptyParas]--> <!--[endif]-->

int main() {

HANDLE hThread1, hThread2;

DWORD retCode;

<!--[if !supportEmptyParas]--> <!--[endif]-->

hThread1 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)&ThreadFunc1,

NULL, 0, NULL);

hThread2 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)&ThreadFunc2,

NULL, 0, NULL);

<!--[if !supportEmptyParas]--> <!--[endif]-->

retCode = WaitForSingleObject(hThread1,3000);

<!--[if !supportEmptyParas]--> <!--[endif]-->

CloseHandle(hThread1);

CloseHandle(hThread2);

<!--[if !supportEmptyParas]--> <!--[endif]-->

if (retCode == WAIT_OBJECT_0 && CriticalData == 1 )

cout << "Success" << endl;

else

cout << "Failure" << endl;

}

<!--[if !supportEmptyParas]--> volatile.cpp

<!--[endif]-->


프로그램 수행은 간단하다. 이 프로그램은 쓰레드를 2개 생성하는데 ThreadFunc1은 Sentinel 플래그가 true인 동안 루프를 돌고, ThreadFunc2는 잠시 기다렸다 Sentinel 플래그를 false로 만들어준다. ThreadFunc2는 Sentinel을 false로 만들기 전에 전역 변수인 CriticalData을 1만큼 증가시킨다..

이 프로그램에서 만약 Sentinel이 volatile로 선언되지 않았다면 ThreadFunc1은 가시성을 보장받지 못하므로, ThreadFunc2가 Sentinel의 값을 바꾸더라도, 레지스터에 든 값을 사용해 영원히 루프를 돌 수 있음은 이미 살펴보았다. 그럼 이번에는 volatile로 선언해서 Sentinel의 가시성이 보장된다면 이 프로그램의 수행 결과는 어떻게 될까?

간단히 생각하면 CriticalData를 1증가 시킨 이후에 Sentinel을 false로 바꾸므로 ThreadFunc1은 1이라는 값을 찍을 것이라고 생각할 것이다. 하지만 CriticalData는 volatile이 아니므로 여전히 메모리가 아닌 ThreadFunc2의 레지스터에만 남아있을 확률이 있다. 이 경우 ThreadFunc1은 변경된 CriticalData가 아닌 0이라는 값이 나올 수 있다. 수행 타이밍에 따라서 0이 되기도 1이 되기도 하는 것이다.

가정을 바꿔서 CriticalData 또한 volatile이라고 해보자. 모든 문제가 해결된 것 같지만, 결과는 여전히 0 혹은 1이 나온다. CriticalData가 volatile이면 레지스터가 아닌 메모리에 직접 쓰므로 가시성은 확보되지만, 재배치의 문제가 남아있다. 컴파일러가 보기에 ThreadFunc2의 CriticalData++과 Sentinel = false는 전혀 관계없는 변수이다. 따라서 최적화를 이유로 이 순서를 뒤집어 Sentinel = false를 먼저 수행하고 CiriticalData=+을 수행할 수 있다. 이 경우 ThreadFunc2에서 Sentinel = false만 수행하고 컨텍스트 스위치(context switch)가 일어난 경우 ThreadFunc1은 아직 CriticalData++이 수행되기 전에 CriticalData 값인 0을 읽을 수 있다.

여기서 Visual C++가 추가한 시멘틱(semantic)을 적용해보자. ThreadFunc2에서 Sentinel = false는 volatile write이므로 프로그램 바이너리에서 그 이전에 수행되어야 할 명령은 모두 volatile write 이전에 수행되게 된다. 따라서 CriticalData++;은 반드시 Sentinel = false; 이전에 수행된다. ThreadFunc1은 Sentinel을 volatile read하므로 그 이후에 실행되는 CriticalData 읽기는 반드시 Sentinel을 읽은 후에 수행된다. 따라서 위 프로그램은 정확히 1을 출력하는 올바른 프로그램이 된다.

또한 재배치가 일어나지 않음을 보장할 경우 Sentinel이 volatile이기만 하면 CriticalData는 volatile이 아니더라도 가시성(visibility)이 보장되는 효과도 있다. 이렇게 다른 volatile 변수로 인해 공짜로 가시성을 얻는 경우 피기백킹(piggybacking, 돼지 등을 타고 공짜로 달린다는 의미)이라고 부른다.

<!--[if !supportEmptyParas]--> <!--[endif]-->

좋은 코딩 습관으로 생각되었다가 재배치 문제로 안전하지 않음이 밝혀진 예로 더블 체크 이디엄(double check idiom)이 있다. 아래 코드처럼 initialized == false로 초기화 여부를 확인하고 객체를 생성해 얻어올 경우 반드시 락을 잡아줘야 한다. read-test-write는 원자적(atomic)이지 않기 때문에 여러 쓰레드가 동시에 초기화를 시작할 수 있기 때문이다. 문제는 한 번 초기화가 된 이후에도 매번 객체를 얻어갈 때마다 락을 잡고 풀어야 한다는 점이다.


Foo* Foo::getInstance()

{

mutex.lock();

<!--[if !supportEmptyParas]--> <!--[endif]-->

if (instance == 0) {

     instance = new Foo();

}

<!--[if !supportEmptyParas]--> <!--[endif]-->

mutex.unlock();

<!--[if !supportEmptyParas]--> <!--[endif]-->

return instance;

}

check.cc


이러한 오버헤드를 피하기 위해 다음과 같은 일단 초기화 여부를 확인한 이후에 실제로 락을 잡아서 다시 한 번 정말 초기화되지 않았는지 확인하는 패턴이 널리 사용되었다. 이 경우는 앞선 예와는 달리 한 번 초기화가 이루어지고 나면 더 이상 락을 잡지 않고 객체를 얻어올 수 있다.

이 코드는 언뜻 보기에 무척 효율적으로 보이지만 재배치와 관련해 큰 문제가 있다. 특히 instance = new Foo(); 의 수행 순서가 문제가 된다. 메모리를 할당 받아 생성자를 호출한 후에 메모리 주소를 instance를 대입한다고 하면 별 문제가 없겠지만, 일부 필드의 초기화 과정과 instance의 포인터 대입의 순서가 컴파일러 재배치로 인해 바뀔 수 있다. 이 경우 아직 일부 필드가 초기화되지 않은 상태에서 instance가 0이 아니게 되므로, 다른 쓰레드가 객체를 얻을 수 있다.


Foo* Foo::getInstance()

{

if (instance == 0) {

     mutex.lock();

<!--[if !supportEmptyParas]--> <!--[endif]-->

     if (instance == 0) {

         instance = new Foo();

}

<!--[if !supportEmptyParas]--> <!--[endif]-->

mutex.unlock();

}

<!--[if !supportEmptyParas]--> <!--[endif]-->

return instance;

}

double_check.cc


정리


지금까지 C/C++의 volatile 키워드의 기본적인 기능과 관련된 문제들을 살펴보았다. volatile은 미묘한 키워드라 잘 알고 쓰면 큰 도움이 될 수 있지만, 또한 여러 가지 문제를 일으키는 근원이 되기도 한다. 특히 명확한 표준이 있는 게 아니므로, 사용하는 자신이 사용하는 C/C++ 컴파일러의 매뉴얼을 꼼꼼히 읽고 volatile을 어떻게 지원하는지 파악하는 게 중요하다.

volatile은 단일 CPU 환경에서 컴파일러 재배치 문제는 해결해주지만, MMU나 멀티CPU에 의한 재배치에 대해서는 완전한 대안을 제공하지 못한다. 또한 변수를 읽은 후에 값을 수정하고 다시 쓰는 read-modify-write를 원자적으로 수행할 수 있게 해주지도 않는다. a += 5; 같은 단순한 명령도 실제로는 a를 메모리에서 읽고 5를 더한 후에 다시 메모리 쓰는 복잡한 연산이므로 a를 volatile로 선언하는 것만으로는 이 코드를 멀티쓰레드에서 안전하게 수행할 수는 없다는 뜻이다. 유용성과 한계를 충분히 인지하고 필요에 따라 적절히 volatile을 사용하자.

<!--[if !supportEmptyParas]--> <!--[endif]-->

참고문헌


[1] The "Double-Checked Locking is Broken" Declaration http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

<!--[if !supportEmptyParas]-->
<!--[endif]-->