문제 환경

  • OS: JetPack 5.1.2
  • HW: Custom Carrier B'd + Jetson Orin NX

 

증상

user@user-desktop:~$ ll /dev/ttyTHS1
ls: cannot access '/dev/ttyTHS1': No such file or directory

 

 

해결 방법

DTB 파일을 Decompile -> Edit -> Compile 순서로 진행한다.

 

Decompile

먼저 본인의 /boot/dtb 폴더에 존재하는 .dtb 파일을 체크한다.

user@user-desktop:~$ ll /boot/dtb/
total 356
drwxr-xr-x 2 root root   4096 Apr 15 15:37 ./
drwxr-xr-x 4 root root  12288 May 20 16:04 ../
-rw-r--r-- 1 root root 346787 Jun 25 10:51 kernel_tegra234-p3767-0000-p3768-0000-a0.dtb

해당 파일을 Decompile하여 디바이스 트리 파일을 생성한다.

# 파일명은 개인마다 다르므로 위에서 확인된 파일을 사용한다.
sudo dtc -I dtb -O dts -o devicetree.dts /boot/dtb/kernel_tegra234-p3767-0000-p3768-0000-a0.dtb

 

 

Edit

De-compile시 경고가 발생하지만 정상적으로 devicetree.dts파일이 생성되었으면 serial@3110000 문구를 검색한다. 그럼 아래와 같은 블록이 보인다.

여기서 status 값을 disabled에서 okay로 변경한다.

serial@3110000 {
    compatible = "nvidia,tegra194-hsuart";
    iommus = <0x06 0x04>;
    dma-coherent;
    reg = <0x00 0x3110000 0x00 0x10000>;
    reg-shift = <0x02>;
    interrupts = <0x00 0x71 0x04>;
    nvidia,memory-clients = <0x0e>;
    dmas = <0x3f 0x09 0x3f 0x09>;
    dma-names = "rx\0tx";
    clocks = <0x02 0x9c 0x02 0x66>;
    clock-names = "serial\0parent";
    resets = <0x02 0x65>;
    reset-names = "serial";
    status = "okay"; # 여기 부분을 disabled에서 okay로 수정한다.
    phandle = <0x2ff>;
};

 

 

Compile

수정 후 dts파일을 다시 dtb로 컴파일하고 기존 파일에 덮어쓴다.

sudo dtc -I dts -O dtb devicetree.dts -o /boot/dtb/kernel_tegra234-p3767-0000-p3768-0000-a0.dtb

 

 

참조

https://forums.developer.nvidia.com/t/missing-dev-ttyths1-for-uart0-uartb-on-nvidia-jetson-orin-nx/249685/7

https://gist.github.com/TobidieTopfpflanze/c162f551ffc4a2be21dbd168371ef347

원글: https://mystes.tistory.com/121


 

주요 특징

std::deque는 std::queue와 유사한 특징을 가집니다. (이하, "데크" 또는 "디큐", "덱")

queue는 FIFO (first in, first out) 구조를 가집니다. 따라서 일반적인 큐는 한쪽에서 자료를 입력하면, 다른 한쪽에서 데이터가 빠져 나오는 구조입니다.

반면 deque는 양쪽에서 데이터의 삽입, 삭제가 가능하기 때문에 FIFO와 더불어 LIFO (last in, first out) 의 특징 역시 가집니다.

일련의 컨테이너와 같이 deque 역시 sequencial(연속적인) 데이터 형태를 가지며 vector의 특징과 동일하게 인덱스를 통한 접근 (랜덤 엑세스), 순회 등과 같은 기능을 제공합니다.

 

기본 사용 예제

기본 예제는 모두 vector와 매우 유사하기 때문에 주요 함수만 알아보겠습니다.

추가 및 삭제

기존 vector에서 컨테이너의 맨 앞에 요소를 추가/삭제할 때 insert 또는 erase 함수를 사용했습니다. 하지만 deque는 위에서 설명한 것처럼 LIFO 특징을 갖기 때문에 push_front, pop_front 함수를 제공합니다.

std::deque<int> q = { 2, 3, 4 };

// 큐의 "맨 앞"에 요소를 추가합니다.
q.push_front(1);  // v : { 1, 2, 3, 4 }

// 큐의 "맨 앞"에 요소를 삭제합니다.
q.pop_front();  // v : { 2, 3, 4 }

 

심화 학습

본문에서 제가 테스트 하는 환경은 ARMv8 Debian입니다. x86 계열이라고 값이 크게 차이나는 것은 아니지만 일부 퍼포먼스나 용량 수치가 변경될 수 있습니다.

stackoverflow에서 이런 말을 본 적이 있습니다.

vector와 deque의 사용처를 고민할 시기가 온다면 당신은 중급 개발자이다.

 

사실, 데크가 할 수 있는 일은 모두 벡터로 대체할 수 있습니다. 심지어 단일 퍼포먼스 측면에서 볼 때 드라마틱한 차이를 보이는 것도 아닙니다.

필자는 주로 15~30 fps 카메라의 이미치 처리를 다룹니다.

 

그럼에도 사소한 차이 하나가 모여 큰 변화를 만들 듯, 의도하는 기능에 적합한 가장 효율적인 방법과 설계를 찾는 것이 중급, 나아가 고급 개발자로서 가져야할 자세이기 때문에 항상 자료구조와 알고리즘이 강조되는 것이 아닐까 생각합니다.

 

vector vs deque

둘의 차이를 일차원적으로 설명하면 다음과 같습니다.

  • 맨 앞에 원소를 추가/제거할 일이 많은 경우, 데크를 사용하자.
  • 그 외 벡터를 사용하자.

 

맨 앞에 원소를 추가할 때 데크가 효율적인 것은 이전 설명에서 어느정도 유추할 수 있었습니다. 전용 함수(push_front)를 제공하는 만큼, 당연히 어느 정도 이점은 있을 것이라 생각했을 것입니다.

실제로 둘의 시간 차이를 비교하면 다음과 같습니다. (테스트는 Google benchmark를 사용하였습니다)

/* compiler = GCC 13.1 std = c++20 optim = O3 */

static void BM_VECTOR(benchmark::State& state) {
    std::vector<int> v;
    while (state.KeepRunning()) {
        state.PauseTiming();
        v.clear();
        state.ResumeTiming();

        for (size_t i = 0; i < 1000; i++)
            v.insert(v.begin(), i);
    }
}
BENCHMARK(BM_VECTOR);

static void BM_DEQUE(benchmark::State& state) {
    std::deque<int> v;
    while (state.KeepRunning()) {
        state.PauseTiming();
        v.clear();
        state.ResumeTiming();

        for (size_t i = 0; i < 1000; i++)
            v.push_front(i);
    }
}
BENCHMARK(BM_DEQUE);

실행 결과는 다음과 같습니다.

deque가 vector보다 21배 정도 빠르다.

테스트 횟수가 적긴 하지만, 일반적으로 데이터를 컨테이너 앞에 삽입하는 작업은 deque가 압도적으로 빠릅니다.

물론 컨테이너 뒤에 삽입하는 경우 vector가 더 빠릅니다. (약 1.2배)

 

벡터의 장점?

vector가 deque에 비해 가지는 이점 중 하나는 메모리의 연속성입니다. deque는 각 요소가 메모리에 연속적으로 존재한다고 보장할 수 없습니다.

이 말은 다시 말해 순회와 같은 메모리 엑세스 시 vector보다 deque가 약간 더 불리하다는 점과, 메모리 효율성 측면에서 떨어진다는 뜻입니다.

실제로 순회의 경우 유의미한 차이를 보이지 않습니다. Benchmark를 진행해도 랜덤 엑세스 시 vector가 미세하게 더 빠르다고 계산되지만 push_front 와 같이 다이나믹한 차이를 보이진 않았습니다.

그러나 다음 코드에서 테스트한 메모리는 꽤 차이가 있습니다.

struct HugeData { char data[8192]; };

int main()
{
  // Vector
  auto vstart = rs::utils::resource::processMemory();

  std::vector<HugeData> vec;
  vec.reserve(1000000);
  for (int i = 0; i < 1000000; ++i)
    vec.push_back(HugeData());

  auto vend = rs::utils::resource::processMemory();
  printf("vector usage : %lu\n", vend.physical_ - vstart.physical_); // 8193601536


  // Deque
  auto dstart = rs::utils::resource::processMemory();

  std::deque<HugeData> que;
  for (int i = 0; i < 1000000; ++i)
    que.push_back(HugeData());

  auto dend = rs::utils::resource::processMemory();
  printf("deque usage :  %lu\n", dend.physical_ - dstart.physical_); // 8216322048
}

실제로 반복 테스트 시 실 사용 메모리는 각 OS에서 취급하는 Memory Management에 따라 값이 어느 정도 변합니다. 하지만 deque가 vector보다 메모리 사용량이 많다는 것을 확인할 수 있으며,

백 만개의 8kb 데이터 기준, 약 22mb의 용량을 더 차지한다는 것을 볼 수 있습니다.

++ 2024-01-15 기준 동작되는 것 확인 (크롬, Brave 브라우저 기준)


 

유튜브를 사용하다보면 내가 검색한 영상과 전혀 무관한, 혐오스러운 영상들이 추가되는데,

특히 "SPA" 붙은 채널들이나 피부외과 시술 영상들(일부 어그로성 썸네일)이 대다수였습니다..

제거하기 위해서 크롬 확장 프로그램을 사용합니다.

 

크롬 뿐만 아니라, 크로미움 기반 대부분 브라우저에서 사용 가능 (크롬 웹스토어 호환 시)

 

 

프로그램 설치

https://chromewebstore.google.com/detail/channel-blocker/nfkmalbckemmklibjddenhnofgnfcdfp

 

Channel Blocker

Allows you to block YouTube™ videos and comments by blacklisting users and/or by using regular expressions.

chrome.google.com

 

 

 

사용 방법 : 직접 등록

일단 확장 프로그램을 설치하고 위 이미지에서 확장프로그램 클릭하면 아래 사진처럼 설정할 수 있는 창이 팝업됩니다.

보기 싫은 채널이 있는 경우, 우측 상단에 채널 이름이나 유저명을 추가해주면 끝입니다.

 

 

사용 방법 : 유튜브에서 지정

확장 프로그램을 설치하면 유튜브에 아래 사진과 같이 "X" 표시가 추가됩니다. 

채널 이름을 등록하는 것이 번거롭다면 X 표시를 클릭해서 채널을 등록할 수 있습니다. 

위 사진은 버튼 UI를 변경했기 때문에 붉은 색인데, 기본 값은 회색 계열이었던 것 같습니다.

 

그런데 경험 상 혐오 채널은 관련 동영상에 자주 등장한다.
관련 동영상은 이상하게 X 표시가 뜨지 않는다.

 

그래서 어쩔 수 없이 채널 이름을 드래그로 잘 복사해서 위에서 말한 직접 등록을 해야 하는 경우가 많았습니다.

물론.. 해당 채널 들어가서 X 표시 생기면 제거해도 되는데, 굳이 클릭했다가 알고리즘 탈까봐 들어가진 않았습니다.

 

 

사용 방법 : 설정 로드

아래 첨부된 설정 파일을 Import Configure를 통해 등록한다. 클릭 -> 파일 선택 -> 확인의 간단한 작업으로 채널 등록이 가능합니다.

Import configuration

A 컴퓨터에서 B 컴퓨터로 설정 값을 복사할 때 주로 사용됩니다.

설정 파일은 종종 업데이트 해보겠습니다..

 

본문에 첨부된 설정 파일은 그냥 제 마음에 안드는 썸네일까지(혐오스럽다고 판단되는) 추가되었으므로,

나는 "외국산 혐오 채널만 등록하고 싶다" 하시는 분들은 직접 등록해야 합니다.

 

240115-Channel_blocker.save
0.00MB

 

 

원글: https://mystes.tistory.com/121


주요 특징

std::vector는 STL에서 가장 인기있는 템플릿 라이브러리 중 하나입니다. 컨테이너의 앞 또는 뒤에 자유롭게 추가 및 삭제가 가능하며 인덱스를 통한 접근(랜덤 액세스) 역시 지원합니다.

 

각 요소는 연속적으로 저장되므로 반복자를 통해 요소에 액세스 할 수 있을 뿐만 아니라 요소에 대한 일반 포인터에 오프셋을 사용하여 요소에 액세스할 수도 있습니다. 즉, 벡터의 요소에 대한 포인터는 배열의 요소에 대한 포인터를 기대하는 함수에 저장될 수 있습니다.

 

벡터의 저장 공간은 자동으로 처리되며 필요에 따라 확장됩니다. 벡터는 향후 증가를 처리하기 위해 더 많은 메모리가 할당되기 때문에 일반적으로 정적 배열보다 더 많은 공간을 차지합니다. 따라서 벡터는 요소가 삽입될 때마다 재할당할 필요가 없고, 추가 메모리가 소진될 때만 재할당합니다. 할당된 메모리의 총량은 capacity 함수를 사용하여 확인할 수 있습니다.

 

재할당은 일반적으로 성능 측면에서 많은 비용을 소모하는 작업으로, 요소의 수를 미리 알고 있는 경우 reserve 함수를 사용하여 재할당을 제거할 수 있습니다.

 

기본 사용 예제

생성 및 할당

벡터의 기본 생성 및 할당은 다음과 같습니다. (주석을 통해 결과 값을 대체합니다)

std::vector<int> v1;                      // v1 : {}
std::vector<int> v2 = { 1, 2, 3, 4, 5 };  // v2 : { 1, 2, 3, 4, 5 }
std::vector<int> v3 = v2;                 // v3 : { 1, 2, 3, 4, 5 }

std::vector<int> v4(v3.begin(), v3.end());  // v4 : { 1, 2, 3, 4, 5 }
std::vector<int> v5(3, 10);                 // v5 : { 10, 10, 10 }
std::vector<int> v6(v5);                    // v6 : { 10, 10, 10 }

std::vector<int> v7(10);     // v7 : { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
std::vector<int> v8(10, 1);  // v8 : { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }

std::vector<int> v9;
v9.assign(5, 2);  // v9 : { 2, 2, 2, 2, 2 }
v9.assign(v7.begin(), v7.end());  // v9 : { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }

std::vector<int> v10;
v10 = { 1, 2, 3, 4, 5 };  // v10 : { 1, 2, 3, 4, 5 }

테스트 중인 환경은 C++20 버전입니다. 하위 버전에서 일부 코드가 동작하지 않을 수 있습니다.

 

추가 및 삭제

벡터에 요소를 추가 및 삭제하는 기본 코드는 다음과 같습니다.

std::vector<int> v = { 1, 2, 4, 5 };

// 벡터의 "맨 끝"에 요소를 추가합니다.
v.push_back(6);  // v : { 1, 2, 4, 5, 6 }

// 벡터의 "맨 앞"에 요소를 추가합니다.
v.insert(v.begin(), 0);  // v : { 0, 1, 2, 4, 5, 6 }

// 벡터의 "3번째 요소 다음"에 요소를 추가합니다.
v.insert(v.begin() + 3, 3);  // v : { 0, 1, 2, 3, 4, 5, 6 }

// 벡터의 "맨 끝"에 요소를 제거합니다.
v.pop_back();  // v : { 0, 1, 2, 3, 4, 5 }

// 벡터의 "맨 앞"에 요소를 제거합니다.
v.erase(v.begin());  // v : { 1, 2, 3, 4, 5 }

// 벡터의 "3번째 요소 다음" 요소를 제거합니다.
v.erase(v.begin() + 3);  // v : { 1, 2, 3, 5 }

// 벡터 모든 요소를 삭제합니다.
v.clear();  // v : { }

 

수정 및 순회

벡터의 요소를 변경하거나 순회하는 방법입니다.

std::vector<int> v = { 1, 2, 3 };

// 벡터 마지막 요소를 반환합니다.
auto n1 = v.back();  // n1 = 3

// 벡터 첫 번째 요소를 반환합니다.
auto n2 = v.front();  // n2 = 1

// 벡터의 요소를 수정합니다.
v[0]    = 10;
v.at(1) = 20;

// 벡터의 요소를 출력합니다.
for (int i = 0; i < v.size(); ++i)
  std::cout << v[i] << ' ';

// 벡터의 요소를 출력합니다.
for (auto it = v.begin(); it != v.end(); ++it)
  std::cout << *it << ' ';

// 벡터의 요소를 출력합니다.
for (auto n : v)
  std::cout << n << ' ';

// 벡터의 크기를 출력합니다.
auto n3 = v.size();  // n3 = 3

 

심화 학습

본문에서 제가 테스트 하는 환경은 ARMv8 Debian입니다. x86 계열이라고 값이 크게 차이나는 것은 아니지만 일부 퍼포먼스나 용량 수치가 변경될 수 있습니다.

push vs emplace

두 방법의 차이는 복사 생성자의 호출 유무입니다.

class Object
{
 public:
  Object() { std::cout << "Object()" << std::endl; }
  ~Object() { std::cout << "~Object()" << std::endl; }
  Object(const Object&) { std::cout << "Object(const Object&)" << std::endl; }
  Object(Object&&) { std::cout << "Object(Object&&)" << std::endl; }
};

int main()
{
  std::vector<Object> v;

  std::cout << "--- emplace_back ---" << std::endl;
  v.emplace_back();

  std::cout << "--- push_back ---" << std::endl;
  v.push_back(Object());

  std::cout << "--- release ---" << std::endl;
}
--- emplace_back ---
Object()
--- push_back ---
Object()
Object(Object&&)
Object(const Object&)
~Object()
~Object()
--- release ---
~Object()
~Object()

Object 객체를 벡터에 추가할 때, emplace_back의 경우 생성자가 1회만 호출됩니다.

그러나 push_back은 먼저 임의의 객체 인스턴스를 생성하고, 그것을 벡터로 이동합니다. 이 과정에서 복사 생성자 및 임의 객체에 대한 소멸자도 호출됩니다.

만약 객체의 생성, 소멸, 복사에 많은 리소스가 소모되는 경우 이 과정은 매우 불필요할 수 있습니다.

 

저장 공간

특징에서 설명했던 것과 같이 벡터의 저장 공간은 동적으로 할당됩니다. 최초 할당되면 일정 수치의 용량을 가지며, 요소가 추가될 때 용량이 증가하는 방식입니다.

코드를 통해 자세히 알아보겠습니다.

#include <iostream>
#include <vector>

int main()
{
  std::vector<int> v;
  auto             cap = v.capacity();

  for (int i = 1; i <= 1000; i++)
  {
    v.push_back(i);
    if (cap != v.capacity())
    {
      std::cout << v.size() << ' ' << v.capacity() << '\n';
      cap = v.capacity();
    }
  }
}

요소를 추가하며, size와 capacity가 어느 시점에 변화하는지, 출력하는 코드입니다. 주요 특징은 capacity가 지수적으로 증가한다는 것을 볼 수 있습니다.

요소의 개수가 일정 수치에 다다르면 내부적으로 Capacity를 증가시킵니다. 자세한 내용은 여기 stack overflow 답변에 자세하게 설명되어 있습니다.

하지만 중요한 점은 vector의 원소가 삭제되었다 할 지라도 늘어난 capacity는 줄어들지 않는다는 점입니다.

v.clear();
std::cout << v.capacity() << std::endl;  // 1024

물론 이것은 크게 문제가 되지 않습니다. 운영체제는 메모리를 관리하는 과정에서 일부 힙 메모리를 재사용하고, vector가 소멸되는 시점에 해당 capacity는 반환되기 때문입니다.

 

벡터의 해제

하지만 벡터가 지역변수가 아닌 전역 또는 멤버 변수로 장시간 유지되는 경우 사용되는 capacity를 반환해 주는 것이 좋습니다.

v.shrink_to_fit();

그 이유는 아래와 같습니다.

struct BigData { char a[1024 * 1024] = {}; };

std::vector<BigData> v;
for (int i = 1; i <= 1000; i++)
  v.push_back({});

v.clear();
printf("clear : %lu ... %lu\n", v.capacity(), rs::utils::resource::processMemory().physical_);

v.shrink_to_fit();
printf("shrink_to_fit : %lu ... %lu\n", v.capacity(), rs::utils::resource::processMemory().physical_);

rs::utils::resource::processMemory() 는 필자가 개발하여 사용하는 SDK로, 현재 프로세스 물리 메모리를 반환하는 함수입니다.

프로세스의 실제 메모리를 출력해보면 clear 이후 메모리가 유지되는 것을 볼 수 있습니다. 만약 벡터가 전역 변수이거나, 프로세스가 살아있는 동안 유지되는 벡터일 경우 해당 메모리는 프로그램이 종료되기 전까지 유지될 것입니다.

따라서 사용하지 않는 벡터의 경우 메모리를 반환하는 습관이 필요합니다.

 

인덱스 접근

벡터의 인덱스에 접근할 때 대표적으로 2가지 방법이 있습니다.

  • at()
  • operator[]

먼저 at[] 보다 상대적으로 빠른 퍼포먼스를 보여줍니다.

int main()
{
  std::vector<int> v = { 1, 2, 3 };

  auto start  = rs::time::tick<nanoseconds>();
  auto res_at = v.at(1);
  auto end    = rs::time::tick<nanoseconds>();
  std::cout << "at: " << (end - start) << "ns" << std::endl;  // 1568ns

  start  = rs::time::tick<nanoseconds>();
  auto res_bracket = v[1];
  end    = rs::time::tick<nanoseconds>();
  std::cout << "bracket: " << (end - start) << "ns" << std::endl;  // 128ns
}

테스트 환경에 따라 결과 값은 다르게 출력됩니다.

rs::time 모듈 역시 필자가 사용하는 개인용 SDK 함수입니다. chrono를 래핑한 함수입니다.

단순 수치만 보면 10배 이상의 퍼포먼스를 보여주지만 operator[]의 치명적인 단점은 예외 처리가 불가능하다는 것입니다.

int main()
{
  std::vector<int> v = { 1, 2, 3 };

  try {
    int res = v[9999];
    std::cout << res << std::endl; // 컴파일러에 따라 다른 쓰레기 값
  } catch (std::exception& e) {
    std::cout << e.what() << std::endl;  // 출력되지 않음
  }
}

위 코드는 v의 존재하지 않는 인덱스를 참조합니다. 그러나 예외는 발생하지 않고 0(gcc) 또는 기타 쓰레기 값(clang)을 반환합니다.

만약 operator[]가 아닌 at을 사용하는 경우 아래처럼 예외가 정상적으로 발생합니다.

vector::_M_range_check: __n (which is 9999) >= this->size() (which is 3)

모든 프로그래머는 자신의 코드가 효율성을 갖추기 위하여 프로그래밍을 하여야 할 의무가 있습니다.

 

알고리즘이란?

설명에 앞서 1부터 100까지 더하는 프로그램을 구현해 봅시다.

아래와 같이 간단하게 구현할 수 있습니다.

int result = 0;
for (int i = 1; i <= 100; i++)
{
  result += i;
}

이것은 C++ 언어를 사용하여 연산을 수행하는 코드입니다. 이 고급 언어를 컴파일, 링킹과 같은 일련의 과정을 거쳐 컴퓨터가 이해할 수 있는 저급 언어로 변환하고, 결과적으로 1부터 100까지 더할 수 있도록 명령하는 과정을 우리는 알고리즘 이라고 합니다.

In mathematics and computer science, an algorithm is a finite sequence of rigorous instructions, typically used to solve a class of specific problems or to perform a computation.

거창하게 설명할 것 없이 컴퓨터로 하여금 특정 명령을 수행하도록 하는 과정을 의미합니다.

 

효율성이란?

그렇다면 방금 작성했던 코드는 효율적인 방법일까요?

제가 생각하는 정답은 모른다 입니다.

효율적이라는 것은 상대적이라 생각합니다.

누군가는 1부터 100까지 더하는 코드를 아래와 같이 구현할 수 있습니다.

int result = (1 + 100) * 100 / 2;

학교 수학 시간에 배웠던 가우스 법칙입니다.

 

이 코드 역시 효율적이다 정의할 수는 없지만, 분명한 것은 반복문을 사용한 코드보다 시간 복잡도 측면에서 효율적인 코드라는 것입니다.

가독성 측면에서 본다면 누군가는 전자의 코드가 더 효율적이라고 볼 수 있기 때문에 시간 복잡도의 측면에서 효율적이라 표현하였습니다.

PC의 연산 성능이 증가하는 요즘, 일부 개발자들은 성능보다 가독성을 우선으로 하는 경우가 있습니다. 저 역시 어느 정도는 동의하지만 가독성은 유지보수의 영역이고, 성능은 운영시점의 영역이기 때문에 둘 사이의 적절한 타협점을 찾는 것이 중요하다고 생각합니다.

 

알고리즘의 성능 측정

알고리즘은 크게 시간 복잡도와 공간 복잡도를 기반으로 그 알고리즘의 성능을 측정합니다.

가독성은 단순히 감성의 영역일 뿐.

시간 복잡도란 코딩 테스트에 단골로 등장하는 내용으로 얼마나 빠르게 동작하는가? 와 관련된 내용입니다.

반면 공간 복잡도란 얼마나 효율적인 리소스로 알고리즘이 동작되는가? 에 관련된 내용입니다.

A라는 기능을 동작하는데 1Gb의 메모리가 필요한 알고리즘은 10Gb의 메모리가 필요한 알고리즘보다 공간 복잡도 측면에서 효율적이라는 뜻입니다.

연산 장치 성능의 발전으로 최근에는 공간 복잡도보다 시간 복잡도가 상대적으로 중요시되지만 임베디드 환경이나 펌웨어 개발, IoT 영역에서는 여전히 공간 복잡도 역시 무시할 수 없는 내용입니다.

 

시간 복잡도와 빅오(Big-O) 표기법

위에서 설명했던 것처럼 효율적이다 라는 것을 수치화 하기 위해서 우리는 시간 복잡도를 사용하고 이것을 Big-O를 통해 표기합니다.

이것을 빅오 표기법이라 부릅니다.

Big-O 표기법은 알고리즘의 런타임 시점에 알고리즘의 상한선을 정의합니다. 다시 말해 알고리즘이 최대로 소요될 수 있는 시간을 의미하며 해당 알고리즘은 그 상한선을 넘지 않아야 합니다. 예를 들어, 동작 시점에 요소 n의 수에 따라 소요 시간이 선형적으로 증가하면 복잡도는 O(n)이 되고, 입력과 무관하게 동일한 소요 시간의 복잡도는 O(1)이 됩니다.

반복문을 통한 1~100 연산은 O(n)이며 가우스 법칙을 통한 방법은 O(1) 입니다.

아래 표는 일반적으로 사용되는 복잡도 수치와 빅오 표기법입니다. 수학적으로 O(x)의 값이 클 수록 소요되는 시간이 늘어나는 것을 의미합니다.

종류 표기법
Constant O(1)
Linear O(n)
Logarithmic O(log(n))
n-log-n O(n ∗ log(n))
Quadratic O(n²)

복잡도가 낮은(빠른) 것은 O(1)이며, 가장 높은(느린) 것은 O(n²)입니다.

O(1) < O(log(n)) < O(n) < O(n * log(n)) < O(n²)

 

이를 그래프로 표현하면 다음과 같습니다.

 

O(1)

이것은 입력 n에 대하여 어떠한 값이 들어오더라도 1회만 실행되는 것을 의미합니다. 다시 말해 알고리즘의 소요 시간은 입력에 대하여 독립적이라는 뜻입니다.

int foo(int n)
{
  return n * (n + 1) * (n + 2);
}

물론 어셈블리 관점에서 n이 0인 것과 n이 INT_MAX 인 경우 연산 차이는 있지만, 반복 관점에서 본다면 차이는 없다.

 

O(n)

이것은 n에 따라 소요 시간이 선형적으로 증가하는 코드를 의미합니다.

대표적으로 아래와 같은 for문이 있습니다.

int foo(int n)
{
  int res = 0;
  for (int i = 1; i <= n; ++i)
    res++;
  return res;
}

int res = 0; 코드와 return res;는 O(1)의 시간을 갖습니다. 반면 for 문과 그 안의 res++ 는, n이 1이라면 1번만 동작되고 종료되지만 n이 1억이라면 foo 함수의 증감연산은 1억번 반복될 것입니다.

소요 시간은 2 + 2 * O(n) 일 수 있으나, 반복 의 관점에서 1 ~ n회가 동작하기 때문에 O(n)으로 표기합니다

 

O(n²)

소요 시간이 가장 큰 알고리즘으로, 이중 for문이 있습니다.

int foo(int n)
{
  int res = 0;
  for (int i = 1; i <= n; ++i)
  {
    for (int j = 1; j <= n; ++j)
      res++;
  }
  return res;
}

1회 for문이 O(n)의 시간 복잡도를 갖기 때문에 당연히 이중 for문은 n의 제곱 복잡도를 갖습니다.

각 라인을 주석을 통해 더 상세히 풀어서 설명하면 아래와 같습니다

int foo(int n)
{
  int sum = 0;                    // ---------------------> c1. N과 무관한 상수 시간 O(1)
  for (int i = 1; i <= n; ++i)    // -----> c2. N에 비례하는 선형 시간 O(n)
    for (int j = 1; j <= i; ++j)  // -> c3. N에 비례하는 선형 시간 O(n)
      sum++;                      // ------------------> c4. N과 무관한 상수 시간 O(1)
  return sum;                     // --------------------> c5. N과 무관한 상수 시간 O(1)
}

c1과 c4, c5는 n과 무관하게 동작하는 코드입니다.

c4 자체(1줄)만 본다면 소요 시간은 O(1)입니다

반면 c2와 c3은 각각 n의 소요 시간을 가지고, 이 것이 중첩되었기 때문에 총 소요되는 시간을 방정식으로 나타내면 다음과 같습니다.

= c1 + c4 + c5 + (n * c2) * (n * c3)
= C + Cn * n²

빅오 표기법은 가장 높은 차수의 시간을 기준으로 표기되므로 해당 알고리즘은 O(n²)으로 표기됩니다.

C++를 주로 사용하는 개발자라면 빼놓을 수 없는 것이 바로 STLContainer입니다.

자료구조를 배워본 사람이라면 각 컨테이너마다 어떤 특징이 있고, 무엇을 기반으로 해당 컨테이너가 개발되었는지 대략적으로 파악할 수 있을 것입니다.

본문에서는 컨테이너에 대한 기본적인 개요와 종류를 알아보고, 향후 각 컨테이너에 대한 자세한 설명과 특징, 장단점 등을 정리해 보겠습니다.

 

컨테이너?

컨테이너는 STL 알고리즘과 결합하여 다양한 데이터 구조를 표현할 수 있도록 설계된 자료형입니다.

각 컨테이너는 자신만의 특징을 가지고, 개발자는 자신이 구현하고자 하는 목적에 맞는 컨테이너를 채택하여 프로그램의 효율성을 높일 수 있습니다.

닭잡는데 소잡는 칼을 사용할 필요는 없습니다

 

효율성?

효율성을 수치화할 수 있는 가장 대표적인 수단입니다. Big-O 표기법에 의거하여 우리는 해당 컨테이너가 얼마의 성능을 가질 수 있는지 나타낼 수 있습니다.

유형 표기법
Constant O(1)
Linear O(n)
Logarithmic O(log(n))
n-log-n O(n ∗ log(n))
Quadratic O(n²)

자세한 내용은 아래 포스트에 상세하게 설명되어 있습니다.

https://mystes.tistory.com/122

 

알고리즘에 대한 고찰 (시간 복잡도)

모든 프로그래머는 자신의 코드가 효율성을 갖추기 위하여 프로그래밍을 하여야 할 의무가 있습니다. 알고리즘이란? 설명에 앞서 1부터 100까지 더하는 프로그램을 구현해 봅시다. 아래와 같이

mystes.tistory.com

 

컨테이너의 종류

컨테이너는 대표적으로 4가지 유형을 갖습니다.

  • Sequence
  • Associative
  • Adaptor
  • Unordered

 

Sequence

모든 요소가 특정 위치를 갖는 정렬된 컨테이너입니다. 시퀀스 컨테이너는 순차적으로 엑세스 할 수 있는 데이터 구조가 특징입니다.

대표적인 시퀀스 컨테이너로는 vector가 있습니다.

 

Associative

연관 컨테이너는 탐색에 최적화된 정렬된 컨테이너입니다. 이것은 O(log N)의 효율성을 가지며 대표적인 연관 컨테이너로는 map이 있습니다.

Associative Container 더 알아보기

 

Unordered associative containers

C++11 이후부터 추가된 연관 컨테이너의 확장 버전으로, 대표적인 타입으로는 unordered map이 있습니다.

이것은 비정렬을 통하여 더욱 빠른 탐색 속도를 보여주지만 삽입 및 삭제의 순서가 보장되지 않습니다.

예를들어 부분 압축 파일을 unordered map에 저장하여 호출할 경우, 압축 파일의 순서가 보장되지 않기 때문에 기대하지 않은 동작이 호출될 수 있습니다.

반면 독립적인 압축 파일을 여러 개 다룬다고 가정할 때, 각 압축 파일은 상호 종속성이 없기 때문에 map보다 효율적인 데이터 구조를 가질 수 있습니다.

Unordered associative containers 더 알아보기

 

Container adaptors

컨테이너 어뎁터의 경우 직접적으로 사용할 수 있지만, 주로 위 3개의 컨테이너를 구현하기 위한 베이스 템플릿으로 사용되는 구조입니다.

알고리즘에 자주 등장하는 queue 또는 stack이 이것에 해당됩니다.

또는 C++23 버전에서 컨테이너 어뎁터 종류 중 4종의 flat 자료형이 추가되었습니다.

flat에 대한 자세한 내용은 여기를 참고하세요.

다중 클라이언트가 접속하는 서버를 구현하는 베이스 코드입니다.

기존 1:1 방식이 아닌, N 명의 클라이언트에 대하여 select를 활용하여 각 클라이언트의 요청을 처리합니다.

현재 진행 중인 웹 브라우저와 리눅스 기반 응용 프로그램 사이, 중계 서버를 만들기 위하여 테스트 한 코드입니다.

 

개요

먼저, 일반적으로 알고 있는 socket의 개념과 별개로 select에 대하여 간략히 설명하자면, select는 네트워크 프로그래밍에서 사용되는 함수 중 하나로,
여러 개의 소켓을 모니터링하고 입출력 가능한 상태를 감지하는 기능을 제공합니다. 이를 통해 동시에 여러 소켓을 관리하고 비동기 네트워크 통신을 구현할 수 있습니다.

일반적으로 select는 유닉스 기반의 운영체제(Linux, mac, FreeBSD 등)에서 제공되는 함수입니다. 하지만 Windows 역시 select 함수 대신 WSASelectEvent 함수 등을 사용하여 이와 비슷한 기능을 구현할 수 있습니다.

확장성 및 여러 포트에서 각각 다른 메세지 처리를 위해 베이스 서버 클래스를 파생하여 여러개의 서버를 생성하도록 설계하였습니다.

하기 모든 코드는 포스팅를 위해 간소화한 코드입니다

 

코드 설명

Server

먼저 베이스로, 모든 서버에서 공통으로 사용되는 변수 및 함수를 정의합니다.

대표적으로 소켓 정보, 소켓 생성, 소켓 종료와 서버 시작 및 종료가 있습니다.

class Server
{
  virtual void running() = 0;  // 서버 루프 메서드

 public:
  Server() = default;
  virtual ~Server();

 public:
  virtual bool start();
  virtual void stop();

 protected:
  // 소켓 제어
  bool openSocket();
  void closeSocket();

 protected:
  int socket_ = MY_INVALID_SOCKET;
};

 

MultiClientServer

Server 클래스를 상속 받아 생성되는 상위 클래스입니다. 이 클래스는 클라이언트의 접속을 수락하여 내부 리스트에 저장하고, 클라이언트 요청을 처리합니다.

class MultiClientServer : public Server
{
  void running() override;

 public:
  virtual ~MultiClientServer() { stop(); }
  const std::vector<ConnectedClient*>& clients() const { return client_list_; }
  const ConnectedClient*               client(int socket);

 public:
  void stop() override;

 private:
  virtual int onReceived(const ConnectedClient* client_info) = 0;

 protected:
  std::vector<ConnectedClient*> client_list_;
};

 

TestServer

마지막으로 실제 서버 구현을 위한 메인 클래스로, 이번 포스트는 설명을 위해 간단하게 에코 서버를 생성합니다.

실제 프로젝트에서는 OPCode 값에 따라 onReceived()에서 메세지를 처리하는 방식으로 진행되는데, 이는 다음 포스트에서 진행합니다.

본 서버는 싱글턴 클래스로 프로그램 종료 시 서버 객체도 같이 삭제됩니다. 지연 생성이 적용되어 있으므로, 생성 시점은 isntance() 가 호출되는 시점입니다.

Server 클래스를 상속 받아 생성되는 상위 클래스입니다. 이 클래스는 클라이언트의 접속을 수락하여 내부 리스트에 저장하고, 클라이언트 요청을 처리합니다.

TestServer실제 프로젝트에서는 OPCode 값에 따라 onReceived()에서 메세지를 처리하는 방식으로 진행되는데, 이는 다음 포스트에서 진행합니다.

class TestServer : public server::MultiClientServer
{
 public:
  static TestServer& instance()
  {
    static TestServer instance_;
    return instance_;
  }

 private:
  TestServer()
  {
    port_ = 9999;
  }

  int onReceived(const ConnectedClient* sClient) override;
};

 

// initialize socket
fd_set fd_reads;
int    fd_max, fd_num;

while (thread_stop_.load() == false)
{
  FD_ZERO(&fd_reads);

  FD_SET(socket_, &fd_reads);
  fd_max = socket_;

  for (unsigned int i = 0; i < client_list_.size(); i++)
  {
    fd_num = client_list_[i]->client_sock;
    FD_SET(fd_num, &fd_reads);
    if (fd_num > fd_max)
      fd_max = fd_num;
  }

  timeval select_timeout = { 1, 0 };

  int activity = select(fd_max + 1, &fd_reads, NULL, NULL, &select_timeout);
  if (activity < 0)
  {
    printf("select() : error : %d : %s\n", errno, strerror(errno));
    break;
  }

  if (activity == 0)
  {
    usleep(10);
    continue;
  }
  ...
}

먼저 fd_set는 일종의 소켓을 담는 컨테이너라고 볼 수 있습니다. 따라서 우리는 소켓을 fd_reads에 담아서 변화를 감지하고 처리하는데, 이 과정을 select 함수가 담당합니다.

서버 메인 스레드가 시작되면 먼저 서버 소켓을 fd_reads에 추가하고, fd_max에 할당합니다. 참고로 fd_max는 현재 관리 중인 소켓들 중 가장 큰 File Descriptor를 의미합니다.

File Descriptor: OS에서 열린 파일, 소켓 또는 파이프와 같은 I/O 리소스를 식별하는 값

 

select 함수는 입력 대기 중인 소켓을 확인하고 결과 값을 반환하는데, 값이 음수인 경우, 네트워크 통신 또는 소켓 세트 이상으로, 스레드를 종료합니다.

또한 블로킹 방지를 위하여 timeout 값을 설정하는데, timeout 시 0이 반환되므로, 필요한 다른 작업을 처리하고(ex. 서버 종료 확인) 다시 대기 확인 상태로 진입합니다.

 

    for (int i = 0; i < client_list_.size(); i++) {
      int client_socket = client_list_[i]->client_sock;

      if (FD_ISSET(client_socket, &fd_reads)) {
        auto sClient = client(client_socket);

        if (sClient) {
          try {
            int ret = onReceived(sClient);
            ...

FD_ISSET은 파일 디스크립터 집합(fd_set)에서 특정 파일 디스크립터가 활성화되어 있는지를 확인하는 함수로, select 함수와 함께 사용하여 어떤 소켓이 활성화되었는지 합니다. select 함수는 활성화된 소켓들의 목록을 fd_set에 설정하고, 이후 FD_ISSET을 사용하여 개별 소켓의 활성화 여부를 확인합니다.

다중 클라이언트 서버와 같은 환경에서 여러 소켓을 관리하면서, 어떤 소켓에서 데이터가 도착했는지 또는 연결 요청이 도착했는지를 확인할 때 사용됩니다.

 

메인 소켓, 다시 말해 socket_의 변화를 FD_ISSET으로 감지하며 클라이언트의 접속 여부를 판단하며, 각 클라이언트 소켓을 FD_ISSET으로 감지하며 메세지 수신 여부를 확인합니다.

만약 클라이언트로부터 0을 수신 받는 경우는 클라이언트와 연결이 종료된 것을 의미하므로 클라이언트 리스트에서 해당 소켓을 삭제합니다.

/* Read NBYTES into BUF from FD.  Return the
   number read, -1 for errors or 0 for EOF. */
extern ssize_t read (int __fd, void *__buf, size_t __nbytes) __wur;

 

int EchoServer::onReceived(const ConnectedClient* client)
{
  std::unique_lock<std::mutex> ulock(message_sync_mutex_);

  uint8_t buffer[PACKET_BUFFER_SIZE];
  memset(buffer, '\0', sizeof(buffer));

  int read_size = read(client->client_sock, buffer, sizeof(buffer));

  if (read_size <= 0) {
    return read_size;
  }

  // 전송 받은 메세지를 다시 클라이언트로 전송합니다
  if (sendMessage(client->client_sock, buffer, read_size) == read_size) {
    printf("echo : %s\n", buffer);
  }

  return read_size;
}

마지막으로 각 서버는 pure virtual method인 onReceived 함수를 통해 수신된 메세지를 처리합니다.

본문에서는 수신된 메세지를 클라이언트로 재전송 하는 기능을 구현합니다.

다음 포스트에서는 실무에서 사용하는 프로젝트들과 유사하게 간단한 Packet을 구성해서 OP-Code에 따른 메세지 처리를 구현할 예정입니다.

 

참고

https://github.com/lasiyan/code-partition/tree/master/multi-client-server-1

GS 인증을 받는 과정 중 암호가 저장된 일부 설정 파일(ini, json 등)에 대한 암호화 과정이 필요했습니다.

본문은 Linux (ARMv8) 환경에서 Crypto++를 설치(빌드)하고 예제 코드를 테스트하는 과정을 기록한 글입니다.

 

테스트 환경

  • Nvidia Jetson Xavier NX (Jetpack 4.6)
  • Ubuntu 18.04 (Linux for Tegra)
  • GCC 7.5.0 (C++17), CMake 3.24

 

설치 과정

먼저 Github를 통해 프로젝트를 다운 받았습니다.

git clone https://github.com/weidai11/cryptopp.git
cd cryptopp

Crypto++는 GNUmakefile을 통해 리눅스 빌드를 지원하고 있었습니다.

저는 현재 개발 중인 프로그램에 내장하기 위해 static 라이브러리 버전을 빌드하였습니다.

OpenCV정도는 아니지만, 시간이 꽤 소요되는 편이기 때문에 CPU 코어를 확인하고 알맞은 스레드 옵션을 주는 것이 좋아 보입니다.

make static -j8 # 자신의 CPU 코어 확인

컴파일이 완료되면 object 파일들이 생성됩니다. 그리고나서 install을 실행하면 이것을 통해 static 라이브러리를 만들고 include 목록을 생성해줍니다.

다행히 install 과정은 compile만큼 오래 걸리진 않았습니다.
저는 root 경로(/usr/local)에 설치되는 것이 싫었기 때문에 경로를 지정해줬습니다.

루트 경로를 사용하려면 sudo 권한이 필요한 것 같습니다.

make install PREFIX=build

# 실행 결과
ls build  # bin  include  lib  share

 

테스트

간단한 테스트를 위해 프로젝트를 하나 생성하였습니다.

아래 코드는 평문을 AES256 알고리즘을 통해 암호화 하여 파일로 저장하고, 이것을 다시 파일에서 읽어 반환합니다.

대칭키는 고정된 문자열을 사용한다고 가정합니다.

 

#include <iostream>
#include <string>

#include "cryptopp/aes.h"
#include "cryptopp/cryptlib.h"
#include "cryptopp/files.h"
#include "cryptopp/filters.h"
#include "cryptopp/hex.h"
#include "cryptopp/modes.h"
#include "cryptopp/osrng.h"
#include "cryptopp/secblock.h"

namespace crypto {
namespace encrypt {
void stringToFile(const std::string& source_string, const std::string& output_file,
                  const std::string& key, const std::string& iv)
{
  try {
    using namespace CryptoPP;

    CBC_Mode<AES>::Encryption encryption;
    encryption.SetKeyWithIV((const byte*)key.data(), key.size(),
                            (const byte*)iv.data());

    // To file
    StringSource ss(source_string, true,
                    new StreamTransformationFilter(encryption,
                      new FileSink(output_file.c_str()))
    );
#if 0 // To source
    std::string encrypted_string = "";
    StringSource ss(source_string, true,
                    new StreamTransformationFilter(encryption,
                      new FileSink(encrypted_string))
    );
    std::cout << "encrypted_string : " << encrypted_string << std::endl;
#endif  
  } catch (const CryptoPP::Exception& e) {
    std::cerr << "encrypt : exception : " << e.what() << std::endl;
  }
}
}  // namespace encrypt

namespace decrypt {
void fileToString(const std::string& source_file, std::string& output_string,
                  const std::string& key, const std::string& iv)
{
  output_string.clear();

  try {
    using namespace CryptoPP;

    CBC_Mode<AES>::Decryption decryption;
    decryption.SetKeyWithIV((const byte*)key.data(), key.size(),
                            (const byte*)iv.data());

    // From file
    FileSource fs(source_file.c_str(), true,
                  new StreamTransformationFilter(decryption,
                    new StringSink(output_string))
    );
  } catch (const CryptoPP::Exception& e) {
    std::cerr << "decrypt : exception : " << e.what() << std::endl;
  }
}
}  // namespace decrypt
}  // namespace crypto

int main()
{
  std::string plane_text = "My crypto @ Testing 1";

  std::string key = "q6wEdPuv3JctZolAi4MbFcQhst89GBLD";  // 32byte 임의의 문자열
  std::string iv = "abcdefghij123456";  // 16byte 임의의 문자열


  // 암호화하여 파일에 저장
  std::string encrypted_file = "encrypted.dat";
  crypto::encrypt::stringToFile(plane_text, encrypted_file, key, iv);

  // 파일을 복호화하여 문자열로 변환
  std::string decrypted_text;
  crypto::decrypt::fileToString(encrypted_file, decrypted_text, key, iv);
  std::cout << "=== Test 1. normal case ================" << std::endl;
  std::cout << decrypted_text << std::endl;


#if 1 // 일반 파일을 복호화 하는 경우
  std::string plane_text_file = "plane-text.txt";
  std::ofstream ofs(plane_text_file);
  ofs << plane_text;
  ofs.close();

  crypto::decrypt::fileToString(plane_text_file, decrypted_text, key, iv);
  std::cout << "=== Test 2. plane text decryption ================" << std::endl;
  std::cout << decrypted_text << std::endl;
#endif

#if 1 // 복호화할 때 키가 잘못된 경우
  key = "abcdefghijklmlnopqrstuvwxyz12345";
  crypto::decrypt::fileToString(encrypted_file, decrypted_text, key, iv);
  std::cout << "=== Test 3. decrypt with wrong key ================" << std::endl;
  std::cout << decrypted_text << std::endl;
#endif
}
=== Test 1. normal case ================
My crypto @ Testing 1
decrypt : exception : StreamTransformationFilter: ciphertext length is not a multiple of block size
=== Test 2. plane text decryption ================

decrypt : exception : StreamTransformationFilter: invalid PKCS #7 block padding found
=== Test 3. decrypt with wrong key ================
�zc��7[|�[�.F�

보다 간단한 이해를 위해 코드에서 예외처리는 대부분 누락되어 있습니다.

 

실제 사용 시에는 Key의 길이를 확인하거나, 초기 벡터(iv)의 길이를 확인하는 로직이 포함되어야 하며, 파일의 존재 유무 역시 판단되어야 합니다.

Xavier 환경에서 직접 개발을 하여도 무관하지만,

편의성을 위하여 개발은 SSH 기반 윈도우 환경에서 Visual Studio Code를 활용하고,

실 테스트는 VNC 또는 실제 보드에서 동작을 진행하였습니다.

 

1. VS Code 설치하기

https://code.visualstudio.com/

 

Visual Studio Code - Code Editing. Redefined

Visual Studio Code is a code editor redefined and optimized for building and debugging modern web and cloud applications.  Visual Studio Code is free and available on your favorite platform - Linux, macOS, and Windows.

code.visualstudio.com

자세한 설치 과정은 생략합니다. (마우스 클릭이 전부..)

 

2. 확장 설치하기

VS Code를 실행하고 좌측 확장 아이콘(Ctrl+Shift+X)에서 아래 플러그인을 설치합니다.

=> Remote - SSH (배포 : Microsoft)

=> Python Extension Pack (배포 : Don Jayamanne)

 

3. 테스트 코드 작성

플러그인이 설치되고 VS Code를 재시작하면 아래 사진과 같이 Remote 아이콘이 생성됩니다.

(그 외 아이콘 목록은 사진과 다를 수 있습니다. 무관합니다.)

해당 아이콘을 클릭하면 아래와 같은 원격 탐색기가 나타나는데, 처음에는 아래 사진과 다르게 아무런 항목도 표기되지 않습니다. 따라서 추가를 해야합니다.

remote configure

추가를 위해 상단 ' + ' 버튼을 클릭하면 SSH 연결을 위한 호스트 정보를 입력하는 프롬프트가 출력됩니다.

아래와 같이 입력합니다.

ssh [사용자명]@[IP주소]     (대괄호 제외!!)

예시 1

다음 창은 구성 정보를 저장할 위치를 입력하는 창입니다. 그냥 첫번째 항목에서 엔터 누르시면 됩니다.

예시 2

그럼 처음과 같이 원격 탐색기에 IP명으로 호스트가 생성되고, 그 위에 마우스 커서를 올리면 +와 폴더가 겹쳐진 아이콘이 출력됩니다.

해당 아이콘을 클릭하여 원격 작업 공간을 열 수 있습니다.

비밀번호를 입력하고 폴더 열기를 통해 원하는 위치에 작업 공간(Work Space)을 생성합니다.

 

4. 인터프리터 설정

저는 이전 다운로드 받았던 yolov5 폴더를 작업 공간으로 설정하였습니다.

그럼 test.py 파일을 하나 생성하여 버전을 출력하는 간단한 코드를 실행해보겠습니다.

# test.py

import sys
print(sys.version)

Ctrl + F5 를 통해 실행하면 현재 실행 중인 파이썬 버전이 출력됩니다.

그러나 우리는 이전 아나콘다를 통해 설치했던 가상 환경에서 개발을 진행할 것입니다.

따라서 인터프리터를 변경해줍니다.

 

먼저 Ctrl + Shift + P 입력하고 Python: Select Interpreter 를 입력합니다.

그럼 아래와 같이 현재 OS(보드)에 설치된 모든 Python 목록이 나타나고,(목록 내용은 사진과 다릅니다!)

여기서 이전 생성했던 가상환경의 Python을 선택합니다.

Recommended가 아닌 Conda 중 선택

이후 다시 코드를 실행하면 아래와 같이 파이썬 버전이 변경되는 것을 볼 수 있습니다.

변경점

 

 

++ 추가

VS Code의 특이점으로, VS Code를 껐다 켜거나 또는 Reload Window 후 다시 Ctrl + F5를 통해 코드를 실행하면

선택한 Interpreter에 해당하는 (가상)환경으로 터미널 창이 변경됩니다.

conda activate에 따른 가상 환경 변경

해당 글에서는 YOLOv5 사용을 위한 PyTorch 및 기타 관련 패키지를 설치합니다.

모든 커맨드는 가상환경이 활성화된 상태에서 진행됩니다. (본문 : darknet)

# 명령
conda activate [환경이름]

# (예시)
conda activate darknet

 


1. PyTorch 설치

일반적인 환경이라면 아나콘다를 통해 간단하게 PyTorch 및 관련 패키지 설치가 가능합니다.

그러나 aarch64 환경에서 이에 맞는 저장소를 찾을 수 없기 때문에 pip를 통해 설치합니다.

주의사항은 원하는 가상환경이 activate 된 상태에서 진행하여야 합니다.

# 설치 (가상환경 베이스)
pip install torch

 

2. YOLOv5 설치

먼저 Git을 통해 YOLOv5를 다운로드합니다.

# (Optional) 작업 디렉토리 생성
mkdir darknet

# Clone YOLOv5
git clone https://github.com/ultralytics/yolov5

다운로드가 완료되면 yolov5 폴더가 생성되며, 해당 폴더 안에 있는 requirements.txt를 실행합니다.

# Yolov5 폴더로 이동
cd yolov5/

# 필요 패키지 설치 (-r : 요구 사항 파일에 기록된 패키지를 설치한다)
pip install -r requirements.txt

설치가 완료되면 "conda list" 를 통해 설치된 패키지를 확인할 수 있습니다.

YOLOv5 패키지 설치 후.. pandas, numpy 등이 설치되었다.

 

3. 동작 확인

패키지가 제대로 설치되었는지 확인하는 간단한 방법입니다.

# YOLOv5 샘플 실행 (yolov5 폴더 내)
python detect.py

처음 실행할 경우, weights 파일이 없기 때문에 yolov5s.pt 파일을 다운로드 진행할 것이고,

기타 모든 옵션은 기본값을 토대로 진행됩니다. 

따라서 위 커맨드는 yolov5/data/images 폴더 안에 있는 2장의 샘플 이미지를 토대로 객체 검출을 진행하며,

실행 결과는 yolov5/runs/detect/exp 폴더 안에 생성됩니다.

detect.py 실행 결과(예시)

 

[개발환경]

- Hardware : Nvidia Jetson Xavier NX (aarch64)

- OS : Jetpack 4.6 rev. 3 (based on Ubuntu 18.04)

 

YOLO 테스트를 위하여 Xavier NX 보드에서 Anaconda(아나콘다)를 사용하기 위한 방법을 정리한 글입니다.

아나콘다는 일반 AMD64 계열은 linux 버전도 지원하지만 라즈베리파이와 같은 aarch64 계열에 대한 지원이 보이지 않아 별도로 다운받아 설치하였습니다.

 


1. 아나콘다 설치

Jetson Anaconda로 검색해보면 Anaconda를 aarch64 버전으로 빌드한 것들이 많았는데,

Mini-forge가 최근 업데이트가 활발한 것으로 보였고, 이를 선택했습니다.

https://github.com/conda-forge/miniforge/releases

 

Releases · conda-forge/miniforge

A conda-forge distribution. Contribute to conda-forge/miniforge development by creating an account on GitHub.

github.com

위 사이트에서 원하는 버전을 받습니다.

글을 작성할 당시 최신 버전은 Miniforge3-4.12.0-0 버전이었습니다.

 

# 다운로드
wget https://github.com/conda-forge/miniforge/releases/download/4.12.0-0/Mambaforge-4.12.0-0-Linux-aarch64.sh

다운로드가 완료되면 해당 스크립트를 실행합니다.

 

# 권한 변경
sudo chmod 777 Mambaforge-4.12.0-0-Linux-aarch64.sh

# 설치
sudo ./Mambaforge-4.12.0-0-Linux-aarch64.sh

conda 경로 설정

모든 경우에 yes 또는 Enter(키)를 입력하고, 중간에 위와 같이 설치 경로(?) 변경 항목은 자유롭게 입력합니다.

(변경이 필요한 경우 내용 입력, 그 외 엔터. 저는 Anaconda라는 이름으로 변경하였습니다)

설치가 완료되면 conda 명령어를 사용할 수 있도록 환경 변수를 등록합니다.

# bashrc 스크립트 열기
vi ~/.bashrc

# 원하는 위치에 아래 명령어 추가
# 필자는 bashrc 맨 하단에 # >>> conda initialize >>> 가 추가되어 있길래 해당 위치에 넣었습니다.
export PATH=/home/rex/Anaconda/bin:$PATH

# 저장 후 재실행
source ~/.bashrc

그럼 아래와 같이 conda 명령어가 동작됩니다.

conda 설치 완료

 

2. 설치 확인

아나콘다 사용을 위해 먼저 가상 환경을 구축해야 합니다.

구축하는 명령은 다음과 같습니다.

# 명령
conda create -n [환경이름] python=[타겟_파이썬버전]

# (예시)
conda create -n darknet python=3.8

(예시) test1 로 생성한 환경. 위 리스트 항목들이 설치된다.

 

생성을 했으면 이제 생성된 환경을 활성화합니다.

# 명령
conda activate [환경이름]

# (예시)
conda activate darknet

# 참고 : 환경 목록 확인하기
conda info --envs

linux 사용자명 앞 환경 이름이 변경되었다.

위 사진과 같이 지정한 가상 환경이 활성화 된 후 우리가 conda 또는 pip로 설치하는 모든 패키지는 해당 환경 위에 설치됩니다.

아래 사진과 같이 똑같은 python 명령어를 입력하여도, 각 버전이 다르게 출력되는 것을 볼 수 있습니다.

환경에 따른 python 버전 차이

MFC 기본 함수 중 DeleteDirectory(..) 함수는 폴더 내 파일 또는 하위 디렉토리가 존재할 경우,

해당 폴더까지 전부 삭제 후 동작된다.

 

따라서 야매로 만든 간단한 방법.

cmd 명령어를 사용하는 코드이다.

void CClientControl::DeleteDirectory_Shell()
{
	CString strCmd;
    
	CString strTarget;
	// strTarget.Format(삭제할 폴더 경로 + 이름);
	// strTarget.Format(_T("E:\\Working\\test_folder")); 예시
	
	strCmd.Format(_T("/c echo Y | cmd /c rd /s %s"), strTarget);
	::ShellExecute(NULL, "open", "cmd", (LPCSTR)strCmd, NULL, SW_HIDE);
}

 

단, rd 명령어는 재귀적으로 지울건지 (Y) 응답을 받는데,

echo Y를 통해 이를 강제로 동작시킨 코드이다. (위험한 커맨드 TOP 10)

 

따라서 반드시 동작 전 strTarget에 정상적인 경로가 들어가는지 확인하고,

절대경로를 사용하도록 하자.

++ 22.04.08 동작 확인
++ 22.10.18 동작 확인
++ 23.06.12 동작 확인

 

 


 

참고한 글 - https://www.clien.net/service/board/lecture/16576594 (클리앙 - 하늘목장님)

 

폴드3/플립3 톰브라운 펌웨어 올리기 & 자급제(KOO)변경하기 : 클리앙

*톰브라운 펌웨어 올리기 = 준비 → 톰브라운펌웨어 올리기 *자급제(KOO)변경하기 = 준비 → 톰브라운펌웨어 올리기 → (설정-일반 들어가서)""초기화"" → 자급제 최신펌 올리기 (→ 초기화) 입니

www.clien.net

 

 

2021년, 11월 12일..

우연히 노트10 플러스 5G 모델을 중고로 얻을 수 있었다....

예전부터 알뜰 요금제만 사용하고, 해외 출장 간 휴대폰을 직구해서 사용했던 사람이라 국내 폰은 처음이었고..

설레는 마음으로 폰을 부팅하였더니 ... ??

 

쓰지도 않는 SKT 로고와 함께, T전화, Txxx 등 여러 통신사 어플이 설치되어 있었다.

 

해외폰을 써본 경험과, AP 메이커에서 일해본 경험을 토대로, 펌웨어부터 바꿔보기로 했는데

결과적으로, 일반적인 방법의 KOO 펌웨어(a.k.a 자급제 펌웨어) 설치는 실패하였다....

 

구글링을 통해 구글 전화를 설치하고 특정 넘버를 입력해서 변경 가능하단 글을 보았다.

시도해봤지만 넘버를 입력해도 반응이 없다..

(찾아보니 안드로이드 보안 버전 패치가 진행됨에 따라 막혔다고 한다.)

 

따라서 Android 11을 10으로 다운그레이드 시도..

안된다... 에러는 (SW REV CHECK FAIL BOOTLOADER 2 BINARY 1)

위 사진을 보면 Bit/SW REV. 컬럼이 보이는데,

현재 최신 버전은 2 버전이고, 따라서 1 버전으로 다운그레이드가 막혀있다.

 

포기하던 중 서두의 글을 보게 되었고,

이를 통해 변경이 가능했다.


작성자분이 아주 자세하게 설명해주셨는데,

간단히 요약하면, (사용한 오딘은 3.14.4 버전이다. 현재 기준 최신)

 

1. Frija 를 통해 노트10+ 스타워즈 에디션 설치

- Model : SM-N976N
- CSC : KOO
- Manual 선택
- PDA : N976NKSU2FUH7
- CSC : N976NOKW2FUH7
- Phone : N976NKOU2FUH5

입력 후 다운로드

 

2. 개발자 옵션에서 OEM 잠금 해제, USB 디버깅 켜기

나에겐 OEM 잠금 해제가 보이지 않았다. 따라서 설정 - 계정 관리에서 모든 계정을 로그아웃 시켰다 - 중요

 

3. 설치

안드로이드 개발하는 사람이라면 adb reboot download 시 자주 보던 화면으로 진입될 것이다.

그게 아니라면, 휴대폰이 꺼진 상태(전원 OFF)에서 좌측 볼륨 키(UP + DOWN)를 동시에 누르고,

그 상태(볼륨키가 눌린 상태)에서 2~3초가 지난 후 휴대폰과 PC를 연결한다. (USB C-Type Cable)

 

참고로 이게 죽어도 안되는 휴대폰이 있다.. MTK에 있을 때 자주 보았는데, 특히 A 시리즈에서 증상이 많았다..
모델에 따라서가 아닌, 복불복이었다.

만약 위 방법으로 진입이 죽어도 안된다면 adb를 설치하고 adb reboot download 커맨드를 사용하자.

 

마지막으로 바이오스로 진입하면 Reboot to bootloader 항목을 선택한다.

 

여기까지 진행하면 스타워즈 에디션이 깔리고, SKT 로고가 사라진다.

하지만 당연히 테마가 스타워즈 느낌으로 깔리기 때문에 이제 다시 일반 자급제로 변경하면 된다.

 

4. 1번과 동일하게 다운받는데,

이번엔 간단하게 Model과 Koo만 입력하고 Auto로 설정하면 일반 자급제로 다운로드가 된다.

 

5. 마찬가지로 개발자 옵션 등 켜주고, 위 2~3번과 동일하게 설치하면

스타워즈가 아닌 일반적인 안드로이드 UI를 확인할 수 있다.

원문 글에서 오딘의 User Data 선택 여부에 따라 초기화가 진행된다고 하는데, 필자 기준으로 어차피 새 중고폰이라 둘 다 User Data를 선택하고 진행했다. 결과적으로 글에서도 마지막 부분에 초기화를 권장하기 때문에 진행 전 백업은 꼭 미리 해두자.

 

오딘을 아예 처음 써보거나, 관련 내용이 생소하신 분들은 서두의 글을 보면 정말 자세하게 설명해주셨기 때문에,

어려움 없이 따라갈 수 있을 것이다.

Nvidia Xavier NX 보드에서 C++ 소스의 컴파일 및 실행을 위한 일련의 과정이다.

 

- 설치 환경
[윈도우] Visual Studio Code
[리눅스] Nvidia Xavier NX Board (Ubuntu 18.04 version)

- 사용 언어
C, C++ with nvcc compiler

 

1. 리눅스에 접속

1. 먼저 윈도우 환경에서 개발을 위하여 VS Code를 설치하고, SSH Remote Control을 통해 원격으로 리눅스에 접속한다.

VS Code의 C++ 확장팩이다. 이것만 받으면 기타 필요한 모든게 해결된다.

 

2. VS Code의 좌측 아이콘 중 "원격 탐색기"를 선택 후 SSH로 리눅스에 접속한다.

경우에 따라 설치 직후 바로 저 Connection Command 창이 뜰 수 있다. 똑같이 입력하면 된다.

만약 원격 탐색기 아이콘이 보이지 않을 경우, VS Code를 재시작 한다.

추가로 SSH 접속 시 기본적으로 config 파일이 자동 생성 되는데, 해당 파일은 윈도우의 "사용자 계정명" 폴더에 생성된다.

만약 사용자 계정이 한글로 설정된 경우 간혹 SSH 연결이 제대로 되지 않은 경우가 있다.

다른 분의 좋은 글이 있어 추가한다.

https://github.com/swsnu/swppfall2020/issues/213

 

3. 연결에 성공하면 VS Code가 재시작되고 아래 사진처럼 폴더 열기 시 원격의 폴더 목록들이 나온다.

원하는 Workspace를 선택하자. VS Code 시작과 동시에 비밀번호 입력 창이 나오는데 리눅스 계정의 비밀번호를 입력해준다.

 

 

2. 빌드 및 실행

일단 리눅스를 써본 적이 없다..

물론 학부 시절 기본적인 Command는 배웠고, 소스 파일 한 두개는 gcc -c ~~.cpp ~~.o,

gcc -o ~~.o ~~ 처럼 빌드해보겠는데, 회사의 소스 코드양은 방대하다. 더구나 다양한 외부 라이브러리, Include 경로 등을 포함한다.

 

이 모든게 Visual Studio에선 "프로젝트 속성" 에서 Addtional include directory 또는 추가 종속성 등 간단하게 처리되고 빌드된다는게...

새삼 IDE의 소중함을 느꼈다.

 

일단 인터넷에 나온대로 tasks.json을 만들었더니,

한 개의 소스 코드만 빌드된다.(당연히 외부 헤더파일들을 찾을 수 없어 에러도 발생한다)

compiler argument에 -I 옵션으로 include 폴더를 추가했다. 역시나 library가 링크되지 않아 오류가 발생된다.

 

구글링을 통해 library 등록까지 하고 빌드해보니 여전히 소스코드는 한 개만 빌드되고,

**.cpp 를 사용해 빌드를 할 수 있다고 하는데, 이 와중에 Makefiles를 알게 되었다.

 

결과적으로 이 Makefile을 자동으로 생성해주고 빌드 해주는 아주 편리한 툴이 CMake라는 것을 알게 되었고,

본문에선 이를 활용한다. tasks.json, Makefile을 직접 작성 하는 것보다 수만배 간단했다.

사내 코드이므로 문제가 될만한 부분은 삭제

구조를 보면 Common에도 많은 파일이 있고,

헤더는 inc, 소스는 src에 담겼으며, root 경로에 main.cpp가 위치한다.

이를 빌드하기 위한 CMakeLists.txt를 생성하고 내용은 아래와 같다.

#요구 CMake 최소 버전
CMAKE_MINIMUM_REQUIRED ( VERSION 3.0 )

# 프로젝트 이름 및 버전
PROJECT ( "AAAAAAA" )
SET ( PROJECT_VERSION_MAJOR 1 )
SET ( PROJECT_VERSION_MINOR 0 )
 
# 빌드 형상
SET ( CMAKE_BUILD_TYPE Debug )
SET ( CMAKE_VERBOSE_MAKEFILE true )

# 컴파일러 세팅
SET ( CMAKE_C_COMPILER "gcc" )
SET ( CMAKE_CXX_COMPILER "/usr/local/cuda/bin/nvcc" )
# 이걸 해줘야 nvcc 빌드 시 -rdynamic unknown 에러가 안남
SET ( CMAKE_SHARED_LIBRARY_LINK_CXX_FLAGS "")

# 소스파일 목록
file( GLOB SOURCES
    Common/*.cpp
    *.cpp
    src/*.cpp
)

# Include 추가 포함 디렉토리
INCLUDE_DIRECTORIES(
    /usr/include/gstreamer-1.0
    /usr/include/glib-2.0
    /usr/local/cuda/targets/aarch64-linux/include
    /usr/include/opencv4
    inc
)
SET ( CMAKE_EXE_LINKER_FLAGS " " )

# 공통 링크 라이브러리 디렉토리
# LINK_DIRECTORIES 사용하면 -Wl,-rpath가 생성되는데, nvcc에서 인식 불가
set (CMAKE_CXX_IMPLICIT_LINK_DIRECTORIES
    /usr/lib/aarch64-linux-gnu/
    /usr/local/lib/
    /usr/local/cuda-10.2/targets/aarch64-linux/lib
)

# 공통 링크 라이브러리
LINK_LIBRARIES (
    pthread
    cudnn
    glib-2.0
    gobject-2.0
    gstapp-1.0
    gstreamer-1.0
    gstrtsp-1.0
    nvinfer
    opencv_core
    opencv_highgui
    opencv_imgcodecs
    opencv_imgproc
    opencv_video
    opencv_videoio
    opencv_calib3d
    opencv_features2d
    cudart
    curand
    cublas
    sqlite3
    jsoncpp
)

# 공통 컴파일 옵션, 링크 옵션
ADD_COMPILE_OPTIONS ( -std=c++11 -w )
SET ( CMAKE_EXE_LINKER_FLAGS " " )

# 출력 디렉토리
SET ( CMAKE_RUNTIME_OUTPUT_DIRECTORY ../bin/${CMAKE_BUILD_TYPE} )


#set(ADD_COMPILE_OPTIONS "")
add_executable(${PROJECT_NAME} ${SOURCES} )

당연히 필요한 부분은 자신에게 맞게 수정하면 된다.

nvcc 관련 특이사항으로 gcc를 이용하면 LINK_DIRECTORY를 사용해서 Library 경로를 명시하라고 많은 예제에서 얘기하는데...

실제로 돌려보면 -Wl,-rpath ~~경로와 같이 콤마(,)도 포함되고, rpath를 nvcc에서 인식하지 못했다.

마찬가지로 기본적으로 빌드를 눌러보면

gcc ~~~ -rdynamic ~~~

와 같이 rdynamic 인자가 자동으로 추가되는데 이 역시 CUDA 10.2 버전의 nvcc에선 unknown options으로 이해하지 못했다.

따라서 Link시 Flag를 제외시켰다.

 

결과적으로 빌드에 성공하였고, 빌드와 디버깅은 아래와 같이 VS Code의 파레트에서 아래 명령을 선택한다.

팔레트 단축키는 <Ctrl> + <Shift> + P 이다. 반드시 CMake를 활용하자

 

우리는 당연히 tasks.json을 수정하지 않았기 때문에 아래 옵션은 동작하지 않는다.(VS Code 기본 빌드)

+ Recent posts