snoopy – 리눅스 커맨드는 history 말고도 별도로 저장해두자. ㅠㅠ

리눅스 커맨드 환경에서 계속 쓰다보면 생각지도 못하게 꼬인 짓들을 할 때가 많다. 그럴 때, 어떤 걸 했는지 헷갈려서 이런 저런 생각 막 하다가 결국 history 쳐보는 게 제일 빠르다는 걸 알긴 한데…..

history도 저장하는 커맨드 수가 한정되어 있다. (근데 이건 당연히 그래야..)

근데 history로도 어떻게 할 수 없는 그런 것들은…. 그냥 커맨드 로거 프로그램 같은 것들로 어떻게 해야 한다. 안그러면 진짜…ㅠㅠ

내 기억력이 그렇게 좋지 않다는 걸 요즘들어서 깨닫고 있는지라..

그래서 snoopy를 써서 요즘은 커맨드 로깅을 좀 하고 있다. 쓰는 법은 github에도 나와있고 해서 뭐 여러모로 편하게 설치해서 쓸 수 있고, 이런 걸로 성능저하 신경쓸 정도도 아니고…

이런 툴들 하나하나 알아둬서 나쁠 건 없으니 알아두자.

ncurses 01 – ncurses 프로그래밍이란?

전에 작업하던 C언어 내용과는 별개로 재미를 붙이기 위해서라도 좀 해야 할 거 같아서 추가로 시작해봅니다. ㅇㅂㅇ

일전에 DOS를 사용하던 분들은 아시겠지만, MC(Midnight Commander)라는 것을 이용하여 콘솔 화면에서도 GUI 처럼 화면을 이리저리 보여주고 방향키로 선택 커서를 옮겨 다니고, 화면에 단축키 처리를 할 수 있던 녀석이 기억 나실지 모르겠습니다. 도스 환경에서는 MDrill, 좀 근래에 썼던 사람들은 mdir이라고 아실텐지… (나이 인증 아닙니다.)

mc-panels.png

이런 비슷한 녀석이요.

당연히 터미널 시스템에서 텍스트 기반으로 쓰던 시스템 환경에서도 화면에 보이는 기술이 중요하다는 것을 알고 있기 때문에, 이런 기반의 기술을 내놓게 됩니다. 바로 Unix System V ver 4.0에 있던 curses라는 기능입니다. 이 기능을 GNU에서 공개해서 어디서든 쓸 수 있도록 한 것이 바로 ncurses입니다.

즉, ncurses 는 콘솔 화면에서 사용자가 이용하기 쉽도록 사용자 친화적인 콘솔 윈도우 화면을 만드는 데 이용되는 라이브러리입니다. 메뉴 바를 제어하거나, 키보드 입력을 돕거나, 윈도우를 띄워주는 그런 기능들을 합니다.

당연히 이런 기능들을 쉽게 이용할 수 있도록 라이브러리화 되어 있고, 함수로도 구현되어 있기 때문에 적절하게 사용하면 됩니다. 그렇다고 해서 뭐 그렇게 큰 환경이 필요한 게 아닙니다. 터미널로 접속 가능한 리눅스만 있으면 충분히 가능합니다.

C언어를 진행하다 보면 지속적으로 물어보는 것이 “이거 어떻게 써요?” 하는 겁니다. 리눅스 시스템 프로그래밍 하는 건 어느 정도 알겠고, 입출력 하는 프로그램이야 당연히 알겠고, 장치 드라이버 짜는 것까지는 알겠는데, 더 이상 어떻게 쓸 곳 없냐고 물어보더군요. 그래서 C 언어를 리눅스에서 프로그래밍하는 것과는 별개의 과정으로 이 과정 또한 조금씩 다뤄보려 합니다. C를 할 줄 안다면 어렵지 않습니다. 오히려 더 좋은 길잡이가 될 수 있으면 좋겠군요.

파일 시스템이 없는 상태에서의 리눅스 커널 부팅

https://youtu.be/ONMhXMxmt20

리눅스 시스템에서 파일 시스템이 없는 경우에 커널 패닉이 일어난다고 했는데, 실제로도 일어나는지를 보여주기 위해서 화면 캡쳐를 통해서 직접 보여주도록 하였다.

Union File System

도커의 이미지와 컨테이너 개념글에서 다뤘던 내용입니다만, 도커는 실행중에 변경된 부분을 이미지로 생성할 수 있다고도 적었습니다. 이게 가능하도록 하는 것이 바로 Union File System(UnionFS)입니다.

이 파일 시스템은 읽기 전용의 파일 혹은 디바이스에서 변경 사항을 기록하여 저장할 수 있도록 해주는 파일 시스템이다. 읽기 전용 파일을 실행할 경우, 해당 파일에 대해 쓰기가 가능한 임시 파일을 생성하여 읽기 파일을 그대로 복제하여 실행을 하게 된다. 그 다음, 수정이 된 내용에 대해 모두 사용하였다면 쓰기 작업을 진행한 다음, 기존의 읽기 파일을 대체한다. 이러한 방식을 Union Mount라고 한다.

도커에서도 해당 이미지 파일을 기본적으로 UnionFS가 지원되는 파일이다. 이 파일을 위의 Union Mount 처리를 하여 지속적인 변경 사항을 작성하여 업데이트 되는 것이다.

UnionFS를 잘 이용한 방식은 리눅스 배포판 중에 크노픽스(Knoppix)라는 배포판이 있다. 이 배포판은 CD 혹은 DVD에서 실행하는 라이브 운영체제 형식으로 배포되는데, 부팅 과정과 운영체제의 기본 실행은 램디스크에서 부팅 및 실행을 하면서도 변경된 사항에 대해서는 컴퓨터에 연결된 보조 저장장치(쓰기 가능)에 저장을 하여 다음에 실행될 때 해당 변경사항을 부팅 후 읽어들여서 적용시킨다.

도커하면 빠지지 않는 기본 이야기: 리눅스 컨테이너

도커에 대한 설명을 하다보면 거의 항상 설명하고 하는 것이 있는데, 바로 LXC(LinuX Container)입니다. 오래전부터, 리눅스/유닉스 환경에서는 chroot라는 명령어를 제공했습니다. 이 명령어는 파일 시스템에서 루트 디렉터리(/)를 변경하는 명령어입니다.

이렇게 특정 디렉토리를 루트 디렉토리로 변경하게 되면chroot jail이라는 환경이 생성되는데, 이 안에서는 바깥의 파일과 디렉터리에 접근할 수 없게 됩니다. 경로 자체가 격리되기 때문에 서버 정보의 유출과 피해를 최소화하는 데 주로 이용되어 왔던 기술입니다. 그러나 chroot는 chroot jail에 들어갈 실행 파일과 공유 라이브러리를 직접 준비해야 하고 설정 방법 또한 상당히 복잡합니다. 또한 완벽한 가상 환경이 아니기 때문에 제약 또한 상당히 많습니다. 이러한 환경을 Confined Environment라고 합니다.

이후에 리눅스에서는 LXC라는 시스템 레벨의 가상화를 제공하게 됩니다. LXC는 컴퓨터를 통째로 가상화하여 OS를 실행하는 것이 아니라 리눅스 커널 레벨에서 제공하는 일종의 격리된 가상 공간을 제공합니다. 이 가상 공간에는 운영체제가 설치되지 않고 호스트의 리눅스를 그대로 이용하기 때문에 가상 머신이라고 안하고 컨테이너라고 합니다. 아래의 이미지가 LXC를 설명하기 가장 좋은 이미지인 듯 하군요.

리눅스 커널의 자원 일부를 그대로 이용합니다. 리눅스 커널의 cgroups(control groups)를 통해 CPU, 메모리, 디스크, 네트웤 자원을 할당하여 완벽한 가상 공간을 제공합니다. 또한 프로세스 트리, 사용자 계정, 파일 시스템, IPC 등을 별도로 격리시켜서 호스트와 별개의 공간을 따로 만듭니다. 이렇게 격리된 곳이 바로 namespaces입니다. 마지막으로 보안 관리를 제공하는 SELinux(Security-Enhanced Linux) 또한 가상화된 공간에서 제공하여 보안도 호스트와 같은 수준으로 제공합니다. 즉, cgroups와 namespaces, SELinux 기능을 활용하여서 가상 공간을 제공하게 되는 겁니다. 추가적으로 필요한 자원이 있다면 드라이버를 가상 환경에 연결하여 지원해줍니다.

하지만, LXC는 격리된 공간에 대한 지원만을 할 뿐, 개발 및 서버 운영에 필요한 부가 기능에 대해서는 상당히 부족한 상황입니다. 도커는 이 리눅스 커널에서의 cgroups, namespaces, SELinux 기능을 기반으로 하여 이미지, 컨테이너의 생성 및 관리 기능과 다양한 부가 기능을 지원할 수 있도록 개발된 것입니다. 그래서 도커에 대한 설명을 하다 보면 자연스럽게 LXC에 대한 설명도 같이 들어가게 됩니다.

도커가 처음 개발될 당시에는 LXC를 통해 구현되었다고 과거 문서들에는 많이 나와있지만, 0.9 버전 이후로부터는 libcontainer를 개발하여 이용하는 것으로 알려져 있습니다. 도커 문서에는 실행 드라이버라고 하는데, native, lxc 중 선택해서 사용할 수 있습니다. lxc는 LXC에서 이용하던 라이브러리입니다. 어떤 것을 이용하는 것이 더 좋은지에 대해서는 여러모로 논의가 이어지고 있습니다만, 양쪽 다 열심히 업데이트 되고 있어서 어떻게 될지는 모르겠군요.

22 – 연산자(관계)

관계 연산자는 좌변과 우변의 값을 비교해서 그 결과를 참(True, 1)과 거짓(False, 0)으로 판명한다.

  • 연산자 / 의미 / 예시
  • == / 같다 / a == b
  • != / 다르다 / a != b
  • < / 작다 / a < b
  • <= / 작거나 같다 / a <= b
  • > / 크다 / a > b
  • >= / 크거나 같다 / a >= b

관계 연산자의 결과값을 받아서 보여주는 예시를 참고한다. 변수에 참과 거짓값을 확인하는 것으로 구성되었다.

(보충) 최신 gcc에서의 선언문 차이

블로그에 작성중인 c의 경우에는 지금 최신의 주분투와 최신 버전의 gcc를 이용하고 있다. 그러나 작성하는 내용은 이전에 규링이 직접 c를 배울 때 쓰던 책 자료도 참고로 하고 있다. 그러다 보면 요즘과는 차이가 좀 나는 부분이 있는데, 이번에 자료를 만들면서 겪었다.

변수 선언을 할 때, 한번에 특정 변수를 동일한 값으로 취급하여 선언하는 경우가 있다. 이전에 작성한 산술 연산자 글에서의 두번째 예시에 보면 a,b,c,d 네 변수를 차례차례 선언한 수에 c=d=2를 작성하여 c,d를 한번에 2로 지정하였다.

근데 c를 오래전부터 짜왔던 분들은 그냥 선언할 때 저 두번째 문장을 확 넣어버리도록 배웠다. 그리고 그 때에는 그대로 했어도 오류가 나지 않았다. 그게 아래에 있는 예시이다.

근데 지금은 이렇게 하면 오류를 낸다.

오류 문장을 보면 d가 선언되지 않았다고 한다. 이게 문제될 것이 뭐냐면서 하는 분들이 있는데, 요즘에는 컴파일러들이 이걸 문제삼는다. 선언을 먼저 함으로써 메모리 할당을 보호하려는 정책인 듯 하다. 자세한 건 찾아봐야 할텐데…;;

21 – 연산자 (산술)

산술 연산자는 수치 계산에 이용되는 연산자로 덧셈, 뺄셈, 곱셈, 나눗셈을 수행한다. 그리고 정수에 대한 나눗셈 연산에서는 소수점 이하는 버린다. 나머지 연산에는 %를 이용하여 나머지만을 나타내며, 정수 연산만 가능하다.

연산자 / 의미 / 예시

  • + / 두 연산자를 더함 / a + b
  • – / 앞의 연산자에서 뒤의 피연산자를 뺌 / a – b
  • * / 두 피연산자를 곱함 / a * b
  • / / 앞의 피연산자에서 뒤의 피연산자를 나눔 / a / b
  • % / 앞의 피연산자에서 뒤의 피연산자를 나눈 나머지 a % b
  • ++ / 피연산자에 1 증가 / a++
  • — / 피연산자에 1 감소 / a–

예시로 +, *를 이용한 예시를 보여준다.

증가 연산자의 경우에는 프로그래밍을 할 때 상당히 많이 이용한다. 사용할 때 조심해야 하는 것이 있는데, ++의 위치이다. a++은 a를 사용한 후에 1을 증가하고, ++a는 a를 사용하기 전에 1을 증가시킨다. 이 둘의 차이를 보여주기 위해서 동일한 식을 보여주자면 아래와 같다.

a = b++;
의 경우에는
a = b;
b = b + 1;
와 같은 식이다.

a = ++b;
의 경우에는
b = b + 1;
a = b;
와 같은 식이다.

이 둘의 차이를 확인하기 위한 예시가 아래의 예시이다. 하나 하나 천천히 실행해서 확인해 본다.

20 – 함수

이 글은 글의 내용이 한번에 쭉 이어져서 글이 좀 길다. 그래서 예시 코드와 실행 결과를 슬라이드 쇼로 만들어서 삽입하였으니 읽을 때 양해를 부탁한다.

프로그램의 시작은 main이라는 함수에서 시작을 한다고 했다. 그러나, 큰 프로그램을 만드는 데 있어서 main 함수 하나로만 작성하면 작성하기도 어려울 뿐더러 일일이 나열하면 반복적으로 작성해야 하는 것들도 많이 생기는데, 이를 일일이 작성할 수는 없다. 이를 위해 작업을 처리하는 단위인 함수로 나눠서 작성하면 소스코드의 길이가 짧아질 뿐만 아니라 다른 프로그램에서도 사용할 수 있도록 만들 수 있다. 함수를 사용하는 예제를 아래와 같이 확인한다.

C언어에서는 main 함수 내에 있는 문장을 순차적으로 실행하는데, 예시 소스의 11번째 라인에 있는 func(); 는 func 함수를 실행하시오라는 의미이다. 그래서 func 함수를 실행을 하고, 함수 실행이 다 끝아면 다시 main 함수의 다음 줄인 12번 줄을 실행한다.

함수 또한 정의를 해야 main 함수에서 알 수 있다. 그래서 main 함수 위에 함수를 작성한 것이 바로 함수를 정의한 것이 된다. 만약 main 함수 밑에 함수를 정의하면 오류를 발생시킬 것이다. 아래의 예시는 직접 오류를 발생시켰다.

이런 경우에는 함수를 별도의 변수처럼 한 줄로 선언함으로써 해결할 수 있다. 함수 선언은 함수 호출 전에 어떤 함수가 정의되어 있는지 먼저 알려주는 것으로, 일반적으로 프로그램 시작 부분에 나타낸다. 선언이 위에 있으면, 함수를 직접 정의하는 부분이 호출하는 함수보다 아래에 있어도 상관이 없다. 지금 학습하는 데 있어서 호출하는 함수는 main 함수이므로, main 함수보다 아래에 함수를 정의하려면 main 함수 위에 함수 선언을 따로 해줘야 한다.

함수를 정의하는 작업을 그냥 위에 하면 되지 않느냐고 묻는 사람들이 많은데, 함수의 수가 많거나 함수간에 호출의 경우에는 어떤 것을 먼저 정의하고 나중에 정의하느냐에 따라서도 구성을 생각해야 하는 경우가 생긴다. 함수가 함수를 부르는 경우에는 특히 더더욱 심각한 문제가 된다. 그래서 미리 헤더를 삽입하는 곳 아래에 함수들을 미리 선언하고 사용한다. 그러면 별도로 이해력이 떨어지는 코드를 짜지 않아도 된다.

함수를 선언하는 예시는 아래와 같이 구성하면 된다.

프로그램은 대부분 여러 개의 함수를 사용한다. 그래서 함수를 하나 더 추가해보도록 한다.

지금까지 작성한 함수는 기존의 main 함수와는 다른 걸 볼 수 있다. 그렇다. return을 마지막에 작성해 주지 않았다. 함수를 실행하고 나면 해당되는 반환값이 없는 것이다. 그래서 void형을 앞에 적어줬다. 그리고 함수를 호출할 때, 어떤 값을 전달해 주지도 않았다. 함수는 반환값과 전달하는 매개변수에 따라서 다음과 같이 분류할 수 있다.

반환하는 값과 매개변수가 없는 함수

반환하는 값은 있고 매개변수는 없는 함수

반환하는 값은 없고 배개변수는 있는 함수

반환하는 값과 매개변수가 있는 함수

우리가 주로 작성하던 main 함수를 보면 반환값에 대한 것은 쉽게 알 수 있다. 앞에 int 라고 작성한 것은 int형 변수를 반환한다는 것이고, 마지막 줄에 있는 return값은 반환할 값이 이것이란 것이다. 그럼 일단 반환값이 있는 함수는 어떤 형식인지 예시로 확인하겠다. 아래 예시에는 return에 5+7이라고 되어 있는데, 5+7을 한 결과값을 반환하는 것이다. 그래서 반환된 값을 main에 있는 i 변수가 받아서 출력을 하니 12가 나온 것이다.

반환값이 없고 매개변수가 있는 함수는 어떤 구조인지 확인하겠다. 매개변수는 함수의 () 안에 넣으면 받아지며, 함수를 선언하고 정의할 때에도 어떤 타입의 값을 받아올 것인지를 정의해줘야 한다.

위의 예제를 보면 함수 정의하고 선언하는 부분에 보면 func(int n1, int n2) 라고 되어 있는데, () 안에 있는 저 두 변수가 매개변수가 된다. 이런 매개변수를 통해 데이터를 주고받을 수 있는데, 특히 호출하는 쪽의 매개변수를 ‘실매개변수’, 함수를 정의하는 쪽의 매개변수를 ‘형식매개변수’라고 한다. 형식매개변수의 경우에는 어떤 형식이라고만 정의해도 되고 그 변수명에 대해서는 정의를 하지 않아도 되는데, 알아보기 쉽게 하기 위해서 일부러 다 정의하는 경우이다.

이젠 매개변수 및 반환값이 전부 존재하는 경우를 예시로 보도록 한다.

마지막으로 매개변수를 이용하지 않을 때를 보도록 한다. ()안에 아무것도 정의하지 않아도 되고, void라고 적어도 된다. 그 예제는 아래에 있다.

함수에 대해서 간단하게 알아보았다. 이를 통해 C언어의 기본적인 내용을 살펴봤다. 이젠 연산과 제어를 위한 작업을 정리할 것이다.

19 – 값을 변경할 수 없는 변수

변수인데도 불구하고 값을 변경할 수 없는 변수가 있다. 선언할 때에만 값을 지정하고 그 이후에는 값을 바꾸지 못하게 하는데, const를 선언할 때 앞에 붙여주면 된다.

const int a = 10;

이렇게 하면 a값은 10으로 고정되어 바꿀 수 없다. 일부러 바꾸려고 하면 오류가 날 것이다. 컴파일러가 오류를 내기 때문에 오류를 직접 예시로 보여주기로 한다.