2018/10/20

Ubuntu 18.10 upgrade


(2019/01/31 추가) 

아래의 Nvidia 드라이버 문제는 다시 구글링 해 보니 쉽게 해결할 수도 있었다.

즉, /etc/gdm3/custom.conf 파일에서 Wayland를 사용하지 않도록 수정해서 저장하면 된다. 아래 내용처럼 해도 되는데 부팅 속도가 좀 느려지는 문제가 있었다.

아무튼, 이런 소소한 점이 문제이긴 하지만, 지금까지 우분투 18.10을 사용해 본 바로는 시스템 안정성이나 성능이나 UI나 매우 만족스럽다.

아래엔 비추천이었지만 강력 추천으로 바뀌었다.

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

어제(현지 시간 10/18) 우분투 18.10 우징어(Cosmic Cuttlefish)가 공식 출시됐다. 데스크탑에서 18.04와 크게 달라진 것은 없지만 Gnome memory leak에 대한 버그를 잡고 GUI 성능 개선에 공을 많이 들였단다. 아무튼 Clean install 보다 Upgrade가 나을 듯해서 업그레이드 했다. 전체 업그레이드 시간은 우분투 Main 서버에서 패키지 받는데 40분, 설치에 40분 해서 80분이 걸렸는데 국내 미러 사이트를 이용하면 1시간도 채 안걸릴듯 하다.

다만, Nvidia 드라이버 문제가 있어서 일반 사용자들에게 우분투 18.10을 추천하기는 어렵다. 이게 새로운 기능이면서 버그일 수도 있다.

우분투 18.10 데스크탑의 새로운 점들

사실은 테마가 Yaru로 바뀌어서 많이 달라진 것 처럼 보인다. Yaru는 일본어로 할 일(To do)이란 뜻이란다(Suru도 같은 뜻인데 친한 사람끼리 쓴다나...). 새로운 점들에 대한 자세한 사항은 우분투 18.10 Release Note를 참고하는 것이 좋겠다.
  • OpenSSL 1.1.1 적용 - TLS v1.3 지원
  • Linux kernel 4.18 탑재 - AMD Radeon, Raspberry Pi, USB 3.2 등 새로운 Hardware 지원
  • 개발 Toolchain 업그레이드 - gcc 8.2, clang 7.0, python 3.6.7/3.7.1 등
  • Gnome 3.30 - dynamic H/W detection, memory leak fix, GUI 성능 개선 등
  • 우분투 커뮤니티가 만든 새로운 테마 Yaru 적용 - 둥글어진 아이콘
  • 노트북 배터리 사용 시간에 대한 고려
  • 지문 인식 스캐너 지원
  • Snap 패키지 지원 강화 - Snapcraft store website를 이용한 sanp 패키지 설치 지원
  • 설치 및 부팅 시간 단축 - 새로운 압축 알고리즘(LZ4/ztsd) 적용
  • Firefox 63.0, LibreOffice 6.1.2 등 수많은 패키지 upgrade
우분투 설치 iso로 부팅/설치시 주의 사항

아래와 같이 initrd에서 예전에 initrd.lz를 사용했는데 그냥 initrd를 사용하는 것으로 바뀌었다.
menuentry "HDD Ubuntu 64-bit iso" {
   set isofile="/boot-isos/ubuntu-18.10-desktop-amd64.iso"
   loopback loop (hd0,9)$isofile
   linux (loop)/casper/vmlinuz boot=casper iso-scan/filename=$isofile noprompt noeject
   initrd (loop)/casper/initrd
}
하드디스크나 USB로 부팅시 부트 메뉴 엔트리를 위와 같이 수정해야 한다. 하드디스크로 부팅시에는 /etc/grub.d/40_custom 파일에서 위의 내용으로 수정해 주고 나서,

$update-grub

하고 재부팅하면 된다.

Nvidia 드라이버 문제

업그레이드 하고 나서 재부팅했더니 fsckd-cancel message...가 뜨면서 먹통이 됐다. 강제로 재부팅했더니 처음엔 문제가 없어 보였는데 혹시나해서 몇번 시험삼아 부팅했더니 똑같이 먹통이 반복된다. fsck가 문제인 줄 알고 복구 모드에서 fsck했더니 아무 문제가 없을 뿐만 아니라 결과도 금방 나온다. 그래서,  Nvidia 드라이버 문제라고 추정하게 됐고 nvidia site에서 410.66 버전을 받아서 390 기본 탑재 버전 지우고 재설치 해봤는데 여전히 먹통이었다.

역시나 30년 가까이 이어져 온 리눅스의 전통이자 매력은 삽질 중독증에 걸리게 만드는 것이다. 이전 글의 방법대로 nouveau 드라이버로 복구하고 나서 여러 가지 시험해 보기로 했다. 문제의 근본 원인은 커널 DRM 모드 셋팅이 제대로 이루어지지 않기 때문으로 추정된다. gdm을 띄우지 못하고 먹통이 되기 때문이다. 그래서 Wayland 사용시 설정했던 아래의 옵션을 사용해 보았다.

options nvidia-drm modeset=1

흠, 잘된다. 여러번 부팅해 봤는데 잘된다. 참고로 위의 옵션에서 modeset=0으로 하면 기본 Nvidia 설정으로 부팅하는 것과 같기 때문에 당연히 먹통이 된다. 단, 위의 설정은 update-initramfs 명령을 사용하기 때문에 우분투 복구모드로 진입하면 그래픽이 깨져서 아무것도 할 수 없는 문제가 생긴다. 그래서, 아예 커널 파라미터로 넣고 부팅하는게 낫겠다 싶었다.

/etc/default/grub 파일에서 아래의 내용과 같이 위의 옵션을 넣어 줄 수 있다.

GRUB_CMDLINE_LINUX_DEFAULT="quiet splash nvidia-drm.modeset=1"

grub 설정이 바뀌어야 하므로 저장한 후 아래의 명령을 해주고 재부팅해야 한다.

$ update-grub

이렇게 해서 Nvidia 문제가 해결됐다. 참고로 410.66 버전을 사용하고 있지만 기존의 390 버전에서도 문제가 없으리라 추정한다. 구글링해 보니 18.10 설치 후 Nvidia 사용자 들의 버그 리포트가 많이 보인다. 그리고, Nvidia 사이트에서 이와 관련된 단서를 하나 찾았는데 Prime 기능 지원에 대한 내용이다.

참고로, 18.04 내지는 17.10이후 <Ctrl>+<Alt>+<F3> ~ <F6>가 콘솔 모드로 진입하는 키이고 <Ctrl>+<Alt>+<F1> ~ <F2>키가 다시 GUI로 복귀하는 키 조합이 되었다.

그리고, 굳이 Nvidia 드리이버를 사용할 필요가 없는 사용자들은 nouveau도 충분히 빨라졌기 때문에 드라이버 설치에 시간을 허비할 필요는 없다. 더구나 Wayland를 사용하면 성능 향상이 눈에 띈다.

ibus 설정 참고 사항

시험삼아 VirtualBox에 우분투 18.10을 설치해 봤는데 설치/부팅 시간이 빨라졌더라. 기본 한글 입력기인 ibus 설정도 잘 된다.

Settings > Region & Language > Manage Installed Languages 를 click 하면 한글 언어 팩이 없그레이드 된다. Gnome Topbar의 사용자 계정을 click 해서 Log Out 했다가 재로그인 한 후,

Settings > Region & Language > Input Sources> [+] 를 click 해서 Korean(Hangul)을 추가해 주면 곧바로 한글 입력이 된다.

물론, fcitx도 잘된다.

기타 참고 사항

Gnome Shell이 3.30이 되면서 Gnome shell extensions가 문제가 될 수 있는데 내가 사용하는 두 가지 모두 처음엔 오류가 떴다.

Hide Top Bar는 다행히 update가 있어서 문제가 사라졌는데  Pixel Saver는 update가 없더라. 다행히, 해당 git 사이트에 갔더니 patch는 올라왔는데 Gnome 사이트에 반영이 안됐단다. 이슈 게시판에 해결책이 올라와 있어서 문제를 해결했다.

그리고, 우분투 18.04 이후 VirtualBox의 guest ubuntu 환경에서도 Wayland를 체험해 볼 수 있다(host 환경과는 무관함).

맺음말

우분투 18.10은 성능 향상과 새로운 테마가 특징이다. 하지만, 불안정한 부분이 남아 있기 때문에 일반 사용자들은 18.04 LTS를 사용하는 것이 좋겠다.

고급 사용자들에게는 Cannonical이 의도한 바는 아니지만, Nvidia 환경에서 Wayland를 사용해 볼 수 있는 기회가 생겼다. 하지만, 화면 잠금 모드로 들어가는 순간 먹통이 되는 문제는 여전하다. Wayland 환경에서 앱들은 하나씩 빠르게 적응해 가는데 상용 비디오 드라이버 지원 문제가 걸림돌이다.

2018/10/14

Signals & Slots - SigLots multithreading 지원


공부도 할겸해서 siglots에 multithread 기능을 넣어 보았다. 구현하면서 부딪치는 문제들을 해결하는 쏠쏠한 재미가 있다. 그 문제 들을 정리하면 아래와 같다.
  • single thread와 multi thread를 동시에 지원하려면? dummy mutex를 만들어서 일종의 template policy로 해결했다.
  • std::atomic_flag를 std::mutex 대신 써보면 어떨까? 기특하게 잘 돌아간다. Signal 객체가 기본적으로 multi thread로 동작하도록 하고 필요시 single thread를 사용할 수 있도록 했다.
  • siglots에 threadpool을 적용해 보면 어떨까? 잘 돌아간다. 덤으로 Qt의 Direct/Queued/ BlockingQueued Connection을 쉽게 구현할 수 있었다. Unique Connection은 siglots가 기본적으로 사용하는 방식이다. 다만 Auto Connection은 나중에 생각해 볼 여지가 있다.
  • Signal 객체에 multi thread를 지원하면서 mutex와 thread 객체를 포함하게 됐기 때문에 Signal 객체 자체는 copy/move가 안된다. 해결 방법은? 나중에 여기를 참고해서...
  • Qt의 QObject::moveToThread()에 대한 생각은? Tracker가 QObject 처럼 시조할배 노릇을 하게되면 구현할 수 있다. 하지만 siglots가 괴물 framework이 될 수도...
  • siglots의 threadpool에서 std::function과 std::packaged_task, std::future 등등을 사용할 필요가 있나? 시간이 되면 나중에 시도해 볼 수도...
  • single이든 multi thread이든 Slot 들의 실행 결과를 활용할 방법은? 필요시 검토...
  • clang++ 6.0.0에서는 lambda가 아무 문제 없는데 g++ 7.3.0에서는 lambda가 rvalue이면서 capture를 사용할때 문제가 생기더라. g++ 버그인가? 좀 찾아보니 lambda는 clang이 좀 안정적인듯... => 참고로, ubuntu 18.10의 gcc/g++ 8.2.0에서는 문제가 해결됐다.

Signal class에서 바뀐 주요 사항

multi thread를 기본으로 사용하면서 Signal class의 prototype이 아래 소스와 같이 바뀌었다. 따라서 single thread를 사용하려면 아래의 예와 같이 변수를 선언해야 한다.

Signal<void(int), SingleThread> signal_single; // for single-threaded Signal
Signal<void(int)> signal_multi;                          // for multi-threaded Signal (default)

아래 소스에서 LockGuard<TM>이 SingleThread/MultiThread의 template parameter 설정에 따라 dummy 또는 atomic mutex를 사용하게 된다. 그리고, 기존의 signal 변수에 SingleThread 옵션을 주면 connect() 함수의 muti thread 관련 설정들은 모두 무시해 버리기 때문에 안전하게 single thread로 실행할 수 있다. 즉, 기존의 multi thread 설정을 single thread로 바꾸려면, Signal 변수 선언 부분의 SingleThread 옵션만 주면 다른 소스 부분을 손댈 필요가 없다. 흠... 물론 Signal 변수 갯수가 밤하늘의 별처럼 많다면(?) 문제가 좀 있겠네... 사실은, 별볼일 없는 세상이고, single thread 신경 쓸 필요 없는 세상일 수도...???

소스에서는 ThreadPool에 thread 2개를 기본으로 사용하고 있지만 1개 이상이면 항상 multi thread를 사용하게 되는 셈이다. CPU core 수에 따라 적절히 조정하면 된다. ThreadPool은 내부적으로 thread queue 1개를 갖고 있다. main thread가 queue에 연결된 Slot들을 넣어 주면 ThreadPool 내의 thread 들이 알아서 일을 해준다. 주의할 점은, main thread는 이후에 자기할 일을 계속하는데, 일(Slots)을 맡겨 놓고 ThreadPool에서 일을 하고 있는 도중에 일을 없애(disconnect) 버릴 수도 있게 된다. 이러면 crash가 생길 수 있다. 그래서 소스에서 보듯이 disconnect() 함수에서 waitforThreads()를 이용해서 ThreadPool의 작업이 종료되지 않은 상황에서는 main thread가 기다렸다가 disconnect()를 수행하도록 하고 있다.

여기서, waitforThreads() 함수 사용시 각별한 주의가 필요하다. milli 초 주기로 ThreadPool이 Slot 실행을 완료했는지 확인하기 때문에 Slot 실행이 금방 끝나는 경우엔 효율적일 수 있지만, 예를 들어 30초 이상 작업 시간이 걸린다면 큰 문제가 될 수 있다. CPU 점유율 100%의 쓴 맛을 보게 될 것이다. std::condition_variable을 사용하는게 좋을 수도 있지만 dead lock 문제가 생길 수 있다. 아무튼 현재 조건에서 최선의 방법은, Slot이 30초 이상 작업을 해야 한다면, 다음에 설명할 BlockingQueued ConnectMode는 사용하지 말아야 한다. 그리고, disconnect() 함수들은 프로그램 종료시가 아니고서는 웬만하면 사용할 필요가 없기 때문에 큰 문제가 되지는 않는다.

connect() 함수에서 Slot의 ConnectMode를 옵션으로 설정할 수 있도록 했고, emit() 함수에서 ConnectMode 또는 SingleThread 여부를 판단해서 signals and slots가 원활히 동작하도록 해준다.

template<typename T, typename TM = MultiThread> class Signal;
template<typename TM, typename TR, typename... TAs>
class Signal<TR(TAs...), TM>
{
  ......
  // Waits for the threads in a ThreadPool to finish work.
  // Disconnecting Slots on which the threads are working may cause disaster.
  void waitforThreads()
  { while(m_threads.running()) std::this_thread::sleep_for(std::chrono::milliseconds(1)); }

  template<typename TO, typename TPMF>
  TypeID connect(TO&& pobj, TPMF&& pmf, ConnectMode mode = Queued)
  {
    LockGuard<TM> lock{m_mutex};
    TypeSlot slot(pobj, std::forward<TPMF>(pmf));
    if(!slot.object()) return 0;
    TypeID id = contains(slot);
    if(!id) {
      slot.setMode(mode);
      m_slots.push_back(slot);
    }
    attachTracker(pobj);
    addToMap(slot.object(), std::forward<TO>(pobj));
    return slot.id();
  }

  // Rvalue-lambda with capture shows abnormal behavior on gcc 7.3.0.
  // Just fine for all cases on clang 6.0.0.
  template<typename TO>
  TypeID connect(TO&& pobj, ConnectMode mode = Queued)
  {
    LockGuard<TM> lock{m_mutex};
    TypeSlot slot(pobj);
    if(!slot.object()) return 0;
    TypeID id = contains(slot);
    if(!id) {
      slot.setMode(mode);
      m_slots.push_back(slot);
    }
    addToMap(slot.object(), std::forward<TO>(pobj));
    return slot.id();
  }
 
  // Disconnects all the Slots connected to this Signal.
  void disconnect()
  {
    LockGuard<TM> lock{m_mutex};
    waitforThreads();
    while(!m_slots.empty()) {
      detachTracker();
      m_slots.pop_back();
    }
  }
  ......
private:
  mutable TM m_mutex;
  ThreadPool m_threads{2};
};

template<typename TM, typename TR, typename... TAs>
void Signal<TR(TAs...), TM>::emit(TAs... args)
{
  LockGuard<TM> lock{m_mutex};
  for(auto it = m_slots.begin(); it != m_slots.end();) {
    ......
    if(it->blocked()) { ++it; continue; }
    if(it->mode() == ConnectMode::Direct || it->mode() == ConnectMode::Auto ||
       std::is_same<SingleThread, TM>::value)
      //TODO: we can do somthing more with returned results if TR is not void.
      //TR result = (*it++).emit(args...);
      (*it++).emit(args...);
    else if(it->mode() & ConnectMode::Queued) {
      //TODO: we can do somthing more with returned results if TR is not void.
      //TR reult = m_threads.run(*it, args...).get();
      m_threads.run(*it, args...);
      if(it->mode() == ConnectMode::BlockingQueued) waitforThreads();
      ++it;
    }
  }
}

siglots에서 multi-thread 사용 예

아래의 소스를 참고하면 Slot ConnectMode가 어떻게 동작하는지 이해할 수 있다. Direct는 Signal에 연결된 Slot 함수를 직접 호출해서 실행한다. Queued는 thread queue에 Slot을 쌓아 놓고 실행되도록 한다. BlockingQueued는 Queued와 동일하지만 ThreadPool이 해당 Slot의 실행을 마친 후에야 Signal을 호출한 thread가 다른 일을 할 수 있다.

아래 소스를 실행해 보면, Signal 객체(sig) 자체가 main thread에서 생성되었기 때문에 single thread와 Direct 방식은 결과가 비슷하다. ThreadPool을 사용하지 않기 때문에 t1과 t2 thread가 일을 한다. BlockingQueued나 Queued는 main thread와 t1, t2 thread, 그리고 ThreadPool의 기본 thread 2개를 합쳐 5개의 thread id가 나타날 것이다. 이 두 방식의 차이는 t2 쓰레드 부분을 모두 comment 처리해서 실행해 보면 확연히 드러난다. 즉, BlockingQueued 방식을 사용하면 Slot 들의 실행 순서를 정해 줄 수 있게 된다.

#include "Signal.h"
std::mutex MX;
#define LOG(x) {std::lock_guard<std::mutex> l{MX}; std::cout << std::this_thread::get_id()\
                 << ' ' << __FUNCTION__ << '(' << __LINE__ << ")> " << x << '\n';}

using namespace siglots;

Signal<void(int, std::thread::id)> sig;
// Signal<void(int, std::thread::id), SingleThread> sig;

void emitN(int n)
{
  for(int i = 0; i < n; ++i) sig.emit(i, std::this_thread::get_id());
}

int main()
{
  auto fl = [](int i, std::thread::id tid) { LOG(i << " - emitter id: " << tid); };

  sig.connect(fl, Queued);
//  sig.connect(fl, Direct);
//  sig.connect(fl, BlockingQueued);
  std::thread t1{emitN, 20};
  std::thread t2{emitN, 10};
  LOG("main thread id: " << std::this_thread::get_id() << '\n');
  LOG("t1 id: " << t1.get_id() << ", t2 id: " << t2.get_id());
  t1.join();
  t2.join();
}

기타 multi-thread 사용시 참고 사항

앞서 multi-thread 환경에서 Slot이 실행되는 중에 그 Slot을 disconnect() 시키면 crash가 생길 수 있다고 했다. 이와 마찬가지로 Slot 객체를 new로 memory allocation 해서 Signal에 연결해서 사용하다가 어디선가 delete 해버리면 crash가 발생하게 된다. 이는 Signal에 국한된 문제는 아니다. 하지만, 이런 문제가 생기면 근본적으로 원인을 찾기가 어렵다. 특히나 multi-thread 환경에서는 엉뚱한 곳에서 crash가 발생하는 듯이 보이기 때문이다. 그니까 raw pointer는 잘 모르고 함부로 사용하면 안된다.

그리고, multi-thread 환경에서는 deadlock이 발생할 수 있는데 crash가 아니고, 말 그대로 프로그램이 멈춰서 동작하지 않는 경우이다. 흔한 경우는 한 thread가 lock을 걸어 놓은 상태에서 다시 lock을 걸 때인데, 함수 내에서 lock_guard를 사용할 때 조심해야 한다. 그 함수 내에서 호출하는 함수에 lock_guard를 다시 설정해 놓는 실수를 하게 된다. 또, 한 가지 흔한 경우는 두 개의 thread가 각각의 자원에 lock을 걸어 놓은 상태에서 상대방 thread의 자원을 요구하는 경우이다. 순환 참조 형태는 늘 문제를 유발시킨다. 순서를 정해 실행되도록 해야 한다.

끝으로, multi-thread 환경에서 공유 자원을 사용할 때는 lock을 사용하는 것이 원칙이지만, 이는 순서대로 thread들이 실행되도록 하는 것이므로 매우 비효율적인 것이기도 하다는 점이다. lock-free multi-threading이 이상적이지만 현실과는 괴리가 좀 있다. 가령, siglots의 ThreadPool이 실행 중인지를 main thread가 알고 싶을때 read-only로 running 상태만 알면 되기 때문에 굳이 lock을 사용할 필요는 없다.