2014년 11월 11일 화요일

OpenSSL supports Multi-threading

내가 스레드를 별로 좋아하지 않는데, 별 수 없이 써야할 경우가 종종 있다. 그와 별개로 OpenSSL 쓰기를 좋아하는데, OpenSSL을 멀티 스레딩 환경에서 쓰면 자칫 알 수 없는 이유로 죽곤한다. 이유는 OpenSSL 각종 알고리즘엔진이 멀티 스레딩 환경을 고려하지 않은 엔진이라, 경합이 발생하여 충돌이 발생하기 때문이다.

참조: https://www.openssl.org/docs/crypto/threads.html

멀티 스레딩 지원은 0.9.5b-dev부터 지원하였으니, 이하 버전은 포기하자. (응?)
(사실 그 전에도 약간은 지원했으나, CentOS5 기준 0.9.8이니, 이전 버전은 포기하는게 정답이다)

맨페이지를 보면 "crypto/threads/mttest.c 파일에 예제가 있어요 뿌잉뿌잉~"이라는데 나중에 찾아보기 귀찮으니 블로그에 옮겨 놓...으려고 봤는데, 주석도 길고 모든 OS에 대한 전처리기도 있고, C++11도 나왔는데 구닥다리 코드를 쓸 생각이 없으니 내 맘대로 다시 구성했다.

#include <openssl/crypto.h>
#define OPENSSL_THREAD_DEFINES
#include <openssl/opensslconf.h>
#if !defined(OPENSSL_THREADS)
# error "OpenSSL version is not supported multi-thread"
#endif


// C++11 mutex support
#include <mutex>

// Global locks for OpenSSL
static std::mutex* g_locks(nullptr);

// Locking callback function for OpenSSL
static
void
funcLock(int mode, int type, char* file, int line)
{
 if ( mode bitand CRYPTO_LOCK ) g_locks[type].lock();
 else g_locks[type].unlock();
}

static
unsigned long
funcThreadId(void)
{
  return static_cast<unsigned long>(std::hash<std::thread::id>()(std::this_thread::get_id()));
}

// Setup function for OpenSSL MT
bool
setupLocks(void)
{
 if ( not g_locks )
 {
  // Create locks
  if ( nullptr == ( g_locks = new std::mutex[CRYPTO_num_locks()] ) ) return false;

  // Set callback function for get thread id as unsigned long
  CRYPTO_set_id_callback(funcThreadId);
  // Set callback function for locking
  CRYPTO_set_locking_callback(funcLock);
 }

 return true;
}

// Main function for example
int
main(int argc, char* argv[])
{
 setupLocks();
 //blar blar blar...
}

별다른 건 없고, std::mutex를 CRYPTO_num_locks() 호출해서 나온 값만큼 만들고, CRYPTO_set_locking_callback()으로 콜백함수를 세팅한다. 콜백함수에서는 주어진 락 인덱스(type)와 잠금해제여부(mode)로 해당 락을 잠그거나 풀어준다.

CRYPTO_num_locks()은 CRYPTO_NUM_LOCKS int형 상수를 반환하는 것 말고는 하지 않는다. 만약 상수가 필요하면 CRYPTO_NUM_LOCKS를 써도 되겠지만, OpenSSL 버전에 따라 상수를 변경할 수 있으므로 가능하면 함수호출과 동적할당으로 처리하자.

살짝 삼천포로 빠지자면, CRYPTO_NUM_LOCKS는 OpenSSL-1.0.1j 기준으로 41이며, 락을 쓰는 곳은 openssl/crypto/lock.c 소스에서 확인할 수 있다. 해당 소스를 까보면 lock_names라는 static 전역변수가 있는데, 이름 그대로 락 이름을 정의해놓았다. CRYPTO_get_lock_name(int type)으로 이름을 받아올 수 있다.

본론으로 돌아와 락을 걸고 푸는 콜백함수는 "void func (int mode, int type, const char* file, int line)" 형태이다.

int mode는 비트 플래그로 어떤 락을 걸고 풀어야할지 알려준다. RW락을 위해 READ/WRITE 상황까지 전달하지만, 예제도 그렇고 대충 걸고 풀고만 체크한다.

CRYPTO_LOCK0x01
CRYPTO_UNLOCK0x02
CRYPTO_READ0x04
CRYPTO_WRITE0x08

int type은 미리 만들어진 여러개 락 중에 어떤 락을 걸지 알려준다.

file, line은 이 함수를 호출한 소스 위치이며, 디버깅을 위해 사용한다.

main함수에 있는 것처럼 setupLocks()를 호출하여 락 초기화와 콜백함수 등록을 한다. 당연히 어플리케이션 시작하고 스레드 만들기 전에 딱 한 번만 하면 되는 일이다. 총총


* OpenSSL을 사용하는 libcurl(물론 gnutls를 사용할 수도 있지만) 같은 라이브러리에서도 저 작업을 사용자(최종 프로그래머)가 하라고 한다.
* libcurl 참조: http://curl.haxx.se/libcurl/c/threaded-ssl.html