Time Namespaces

Time Namespace를 컨테이너 체크포인트/복원과 시간 일관성 보장 관점에서 심층 분석합니다. timens_offsets 구조체의 내부 필드와 커널 적용 경로, vDSO/VVAR 페이지 분리 메커니즘과 clock_gettime() 오프셋 적용 체인, CRIU 기반 라이브 마이그레이션에서 시간 연속성을 보장하는 원리, futex/timerfd/nanosleep/timer_create에 대한 시간 네임스페이스 영향, 호스트-게스트 시간 경계 관리, 관측 지표 왜곡을 줄이기 위한 운영 패턴, 디버깅 시 clock_gettime 비교 검증 절차까지 시간 격리가 필요한 컨테이너 환경의 실전 포인트를 다룹니다.

문서 통합 안내: Namespaces 개요 문서에 Time Namespace 요약이 통합되었습니다. 전체 맥락은 Namespaces - Time Namespace 통합 요약에서 먼저 확인하세요.
전제 조건: 네임스페이스시간 관리 문서를 먼저 읽으세요. 시간 네임스페이스는 절대 시간이 아니라 보이는 기준 시각 오프셋을 분리하는 개념이므로 clock 종류별 영향 범위를 먼저 확인해야 합니다.
일상 비유: 이 주제는 같은 역에서 서로 다른 표준시 안내판과 비슷합니다. 실제 시계는 하나여도 안내판 기준을 다르게 보여주듯이, 컨테이너별 시간 관측값을 분리해 재현성과 격리를 확보합니다.

핵심 요약

  • 격리 대상CLOCK_MONOTONIC, CLOCK_BOOTTIME 및 관련 변종 오프셋
  • 비격리 대상CLOCK_REALTIME은 전역 시간 공유 (의도된 설계)
  • timens_offsets — monotonic과 boottime 각각의 오프셋을 저장하는 핵심 구조체
  • vDSO/VVAR — 시스템 콜 없이 userspace에서 시간을 읽을 때도 오프셋 적용 보장
  • CRIU — 체크포인트/복원 시 시간 연속성 유지의 핵심 소비자
  • frozen — 첫 프로세스 진입 후 오프셋 변경 불가 (일관성 보장)

단계별 이해

  1. 지원 시계 구분
    어떤 clock이 격리되고 어떤 clock은 공유되는지 먼저 표로 고정합니다.
  2. 오프셋 모델 이해
    절대 시간을 바꾸는 것이 아니라, monotonic/boottime 읽기값에 오프셋을 더하는 모델임을 확인합니다.
  3. 실습으로 검증
    네임스페이스 진입 전후 clock_gettime() 값을 비교해 격리 동작을 관찰합니다.
  4. 복원 시나리오 연결
    CRIU 복원 시 동일한 시간축을 재현해야 하는 이유를 운영 관점에서 정리합니다.
격리 경계 주의: Time namespace는 CLOCK_MONOTONIC/CLOCK_BOOTTIME 계열의 관측값을 분리하지만, CLOCK_REALTIME까지 숨기는 기능은 아닙니다. 따라서 "시간 오프셋 재현"과 "wall clock 은닉"은 별개의 요구사항으로 다뤄야 합니다.
관련 문서: Namespaces (namespace 개요), Linux Containers (컨테이너 격리), Timers & Timekeeping (시간 관리), ktime/Clock (커널 시계), System Call (clock_gettime), vDSO (가상 동적 공유 객체)

개요

Time Namespace는 Linux 5.6(2020년 3월)에서 도입된 8번째 namespace 타입으로, CLOCK_MONOTONICCLOCK_BOOTTIME 시계의 오프셋을 프로세스별로 격리합니다. 커널 소스에서는 kernel/time/namespace.c(약 500줄)에 핵심 로직이 집중되어 있습니다.

도입 배경

동기문제Time Namespace 해결
CRIU 복원 Host B에서 복원 시 monotonic clock이 불연속 오프셋으로 원래 값 재현
컨테이너 격리 모든 컨테이너가 호스트의 uptime을 공유 컨테이너별 독립 uptime 제공
시간 의존 테스트 미래 시간 시뮬레이션 불가 monotonic 오프셋으로 시간 앞당기기/되돌리기
보안 호스트 부팅 시간이 컨테이너에 노출 오프셋으로 실제 부팅 시간 은닉

지원하는 Clock 상세

Clock ID 설명 Time NS 영향 오프셋 소스
CLOCK_REALTIME UTC 기반 실시간 (wall clock) 격리 안 됨 없음
CLOCK_MONOTONIC 부팅 이후 단조 증가 (suspend 제외) 격리됨 offsets.monotonic
CLOCK_MONOTONIC_RAW NTP 조정 없는 monotonic 격리됨 offsets.monotonic
CLOCK_MONOTONIC_COARSE 낮은 정밀도 monotonic 격리됨 offsets.monotonic
CLOCK_BOOTTIME 부팅 이후 (suspend 포함) 격리됨 offsets.boottime
CLOCK_BOOTTIME_ALARM 부팅 시간 기반 알람 격리됨 offsets.boottime
CLOCK_REALTIME_ALARM 실시간 기반 알람 격리 안 됨 없음
CLOCK_PROCESS_CPUTIME_ID 프로세스 CPU 시간 격리 안 됨 없음
CLOCK_THREAD_CPUTIME_ID 스레드 CPU 시간 격리 안 됨 없음
주의: CLOCK_REALTIME은 time namespace의 영향을 받지 않습니다. 이는 의도된 설계입니다 -- 파일 타임스탬프, TLS 인증서 검증, 네트워크 프로토콜 등이 전역적으로 일관된 wall clock을 필요로 하기 때문입니다.

아키텍처

Userspace clock_gettime() timerfd_create() nanosleep() /proc/uptime vDSO 경로 (syscall 불필요) __vdso_clock_gettime() → VVAR 페이지에서 오프셋 읽기 syscall 경로 sys_clock_gettime() → timens_ktime_to_host() task_struct nsproxy->time_ns nsproxy->time_ns_for_children time_namespace timens_offsets (mono/boot) vvar_page, frozen VVAR 페이지 vdso_data (시간 데이터) namespace별 별도 매핑 /proc/[pid]/timens_offsets monotonic [sec] [nsec] | boottime [sec] [nsec]

time_namespace 구조체

/* kernel/time/namespace.c — Linux 6.x */
struct time_namespace {
    struct user_namespace *user_ns;   /* 소유 user namespace */
    struct ucounts        *ucounts;   /* namespace 카운트 */
    struct ns_common      ns;          /* 공통 namespace 정보 */
    struct timens_offsets offsets;     /* clock 오프셋 (핵심) */
    struct page          *vvar_page;  /* namespace별 VVAR 페이지 */
    bool                  frozen_offsets; /* 오프셋 변경 가능 여부 */
};

/* 오프셋 구조체 */
struct timens_offsets {
    struct timespec64 monotonic;   /* CLOCK_MONOTONIC 오프셋 */
    struct timespec64 boottime;    /* CLOCK_BOOTTIME 오프셋 */
};

/* timespec64: 64비트 시간 표현 */
struct timespec64 {
    time64_t tv_sec;    /* 초 (음수 가능 = 과거 오프셋) */
    long     tv_nsec;   /* 나노초 (0 ~ 999,999,999) */
};

Task 구조체 통합

struct nsproxy {
    struct uts_namespace    *uts_ns;
    struct ipc_namespace    *ipc_ns;
    struct mnt_namespace    *mnt_ns;
    struct pid_namespace    *pid_ns_for_children;
    struct net             *net_ns;
    struct cgroup_namespace *cgroup_ns;
    struct time_namespace  *time_ns;              /* 현재 time NS */
    struct time_namespace  *time_ns_for_children; /* 자식용 time NS */
};

/* unshare(CLONE_NEWTIME) 호출 시:
 * 1. 새 time_namespace 할당
 * 2. current->nsproxy->time_ns_for_children = new_ns
 * 3. current->nsproxy->time_ns는 변경되지 않음 (부모는 기존 NS 유지)
 * 4. fork() 후 자식의 time_ns = time_ns_for_children
 */
중요: unshare(CLONE_NEWTIME) 호출 프로세스 자체는 새 time namespace에 들어가지 않습니다. 자식 프로세스만 새 namespace에 진입합니다. 이는 실행 중인 프로세스의 VDSO 페이지 매핑을 안전하게 전환할 수 없기 때문입니다.

Clock Offset 관리

timens_offsets 파일

Time namespace의 오프셋은 /proc/self/timens_offsets 파일로 설정합니다. 이 파일은 proc_timens_offsets_operations에 의해 관리됩니다.

# 파일 형식: clock_name seconds nanoseconds
$ cat /proc/self/timens_offsets
monotonic           0         0
boottime            0         0

# 오프셋 설정 (frozen 전에만 가능)
$ echo "monotonic 1000 0" > /proc/self/timens_offsets
$ echo "boottime 2000 500000000" >> /proc/self/timens_offsets

오프셋 쓰기 커널 경로

write(/proc/.../timens_offsets) proc_timens_offsets_write() timens_offsets_write() 파싱 frozen_offsets 체크 (true → -EACCES) nsec 범위 체크 (0~999999999) offsets 갱신 성공 timens_setup_vvar_page() VVAR 페이지에 오프셋 반영

오프셋 적용 시점과 조건

단계동작상태
1. unshare(CLONE_NEWTIME) 새 time_namespace 생성, time_ns_for_children에 할당 frozen=false
2. write timens_offsets 오프셋 값 설정 (여러 번 덮어쓰기 가능) frozen=false
3. fork() 자식이 새 time_ns로 진입, VVAR 페이지 매핑 frozen=true (이 시점부터)
4. 이후 write 시도 -EACCES (Device or resource busy) frozen=true
/* 커널에서 frozen 체크하는 코드 */
static ssize_t timens_offsets_write(struct file *file,
    const char __user *buf, size_t count, loff_t *ppos)
{
    struct time_namespace *ns = current->nsproxy->time_ns_for_children;

    /* 이미 frozen이면 쓰기 거부 */
    if (ns->frozen_offsets)
        return -EACCES;

    /* "monotonic sec nsec" 또는 "boottime sec nsec" 파싱 */
    ...
    return count;
}
실무 팁: 오프셋은 음수도 가능합니다. CRIU 복원 시 원본 호스트보다 대상 호스트의 monotonic 값이 클 경우 음수 오프셋을 사용합니다. 예: 원본 100초, 대상 5000초 → 오프셋 = -4900초.

timens_offsets 구현 상세

timens_offsets 구조체는 매우 단순하지만, 커널 전반에 걸쳐 다양한 위치에서 참조됩니다. 오프셋이 적용되는 정확한 코드 경로와 조건을 분석합니다.

/* kernel/time/namespace.c — 오프셋 적용 헬퍼 함수들 */

/* monotonic 시간에 오프셋 추가 */
void timens_add_monotonic(struct timespec64 *ts)
{
    struct timens_offsets *ns_offsets;

    /* init namespace에서는 오프셋 0 → 빠른 경로 */
    if (likely(!timens_offset_exists(current)))
        return;

    ns_offsets = &current->nsproxy->time_ns->offsets;
    *ts = timespec64_add(*ts, ns_offsets->monotonic);
}

/* boottime 시간에 오프셋 추가 */
void timens_add_boottime(struct timespec64 *ts)
{
    struct timens_offsets *ns_offsets;

    if (likely(!timens_offset_exists(current)))
        return;

    ns_offsets = &current->nsproxy->time_ns->offsets;
    *ts = timespec64_add(*ts, ns_offsets->boottime);
}

/* 절대 시간을 호스트 시간으로 역변환 (타이머/sleep) */
ktime_t timens_ktime_to_host(clockid_t clockid, ktime_t tim)
{
    struct time_namespace *ns;
    struct timens_offsets *offsets;
    ktime_t offset;

    ns = current->nsproxy->time_ns;
    if (likely(ns == &init_time_ns))
        return tim;

    offsets = &ns->offsets;

    switch (clockid) {
    case CLOCK_MONOTONIC:
    case CLOCK_MONOTONIC_RAW:
    case CLOCK_MONOTONIC_COARSE:
        offset = timespec64_to_ktime(offsets->monotonic);
        break;
    case CLOCK_BOOTTIME:
    case CLOCK_BOOTTIME_ALARM:
        offset = timespec64_to_ktime(offsets->boottime);
        break;
    default:
        return tim;  /* REALTIME 등은 변환 없음 */
    }

    /* NS 시간 - 오프셋 = 호스트 시간 */
    return ktime_sub(tim, offset);
}

/* 빠른 경로 체크: init NS인지 확인 */
static inline bool timens_offset_exists(
    struct task_struct *task)
{
    return task->nsproxy->time_ns != &init_time_ns;
}

procfs 인터페이스 커널 구현

/* proc_timens_offsets_write() — 오프셋 쓰기 전체 경로 */
static ssize_t proc_timens_offsets_write(
    struct file *filp,
    const char __user *ubuf,
    size_t count, loff_t *ppos)
{
    struct proc_timens_offset offsets[2];
    char *kbuf;
    int noffsets, ret;

    /* 대상 프로세스의 time_ns_for_children 가져오기 */
    struct time_namespace *ns =
        current->nsproxy->time_ns_for_children;

    /* 이미 frozen이면 거부 */
    if (ns->frozen_offsets)
        return -EACCES;

    /* 권한 확인: CAP_SYS_TIME 필요 */
    if (!ns_capable(ns->user_ns, CAP_SYS_TIME))
        return -EPERM;

    /* 사용자 공간에서 문자열 복사 */
    kbuf = memdup_user_nul(ubuf, count);

    /* "monotonic sec nsec" 또는 "boottime sec nsec" 파싱 */
    noffsets = parse_timens_offsets(kbuf, offsets, ARRAY_SIZE(offsets));

    /* 값 검증 */
    for (int i = 0; i < noffsets; i++) {
        /* nsec 범위: 0 ~ 999,999,999 */
        if (offsets[i].val.tv_nsec < 0 ||
            offsets[i].val.tv_nsec >= NSEC_PER_SEC)
            return -EINVAL;
    }

    /* 오프셋 적용 */
    mutex_lock(&offset_lock);
    for (int i = 0; i < noffsets; i++) {
        switch (offsets[i].clockid) {
        case CLOCK_MONOTONIC:
            ns->offsets.monotonic = offsets[i].val;
            break;
        case CLOCK_BOOTTIME:
            ns->offsets.boottime = offsets[i].val;
            break;
        }
    }
    mutex_unlock(&offset_lock);

    return count;
}

오프셋 동결(frozen) 메커니즘

오프셋 동결은 첫 번째 프로세스가 새 time namespace에 진입하는 시점에 자동으로 발생합니다. 구체적으로 timens_on_fork()에서 frozen_offsetstrue로 설정됩니다.

/* fork() 시 time namespace 처리 */
void timens_on_fork(struct nsproxy *nsproxy,
                    struct task_struct *tsk)
{
    struct time_namespace *ns =
        nsproxy->time_ns_for_children;

    /* 현재 NS와 자식용 NS가 다르면 = 새 NS로 전환 */
    if (nsproxy->time_ns == ns)
        return;

    get_time_ns(ns);
    put_time_ns(nsproxy->time_ns);
    nsproxy->time_ns = ns;

    /* 오프셋 동결: 이후 쓰기 불가 */
    ns->frozen_offsets = true;

    /* VVAR 페이지 설정: vDSO에서 오프셋 접근 가능하게 */
    timens_commit(ns);
}
time unshare() CLONE_NEWTIME frozen=false write offsets monotonic +1000 frozen=false write offsets 덮어쓰기 가능 frozen=false fork() 자식이 NS 진입 frozen=true write 시도 -EACCES 반환 frozen=true 오프셋 변경 불가 구간 (frozen=true) 오프셋 변경 가능 구간 (frozen=false)
주의: frozen_offsets는 한번 true가 되면 되돌릴 수 없습니다. 새로운 오프셋이 필요하면 새 time namespace를 생성해야 합니다. 이 설계는 이미 NS에 진입한 프로세스의 시간이 갑자기 바뀌는 것을 방지합니다.

System Call 통합

clock_gettime() 오프셋 적용 체인

/* 커널 내부 오프셋 적용 흐름 (간략화) */

/* 1. 시스템 콜 진입 */
SYSCALL_DEFINE2(clock_gettime, const clockid_t, which_clock,
                struct __kernel_timespec __user *, tp)
{
    struct timespec64 kernel_tp;
    kc->clock_get_timespec(which_clock, &kernel_tp);
    /* 여기서 오프셋이 이미 적용됨 */
    return put_timespec64(&kernel_tp, tp);
}

/* 2. posix clock 구현에서 timens 오프셋 적용 */
static int posix_get_monotonic_timespec(clockid_t which_clock,
                                        struct timespec64 *tp)
{
    ktime_get_ts64(tp);  /* 실제 하드웨어 시간 */
    timens_add_monotonic(tp);  /* 오프셋 적용 */
    return 0;
}

/* 3. 오프셋 적용 함수 */
void timens_add_monotonic(struct timespec64 *ts)
{
    struct timens_offsets *ns_offsets =
        &current->nsproxy->time_ns->offsets;

    *ts = timespec64_add(*ts, ns_offsets->monotonic);
}

/* boottime도 동일한 패턴 */
void timens_add_boottime(struct timespec64 *ts)
{
    *ts = timespec64_add(*ts,
        current->nsproxy->time_ns->offsets.boottime);
}

영향받는 시스템 콜

시스템 콜영향오프셋 적용 위치
clock_gettime()MONOTONIC/BOOTTIME 계열 오프셋 적용timens_add_monotonic/boottime()
clock_nanosleep()절대 시간 지정 시 오프셋 역변환timens_ktime_to_host()
timerfd_settime()절대 타이머 시 오프셋 역변환timens_ktime_to_host()
timer_settime()POSIX 타이머 절대 시간 역변환timens_ktime_to_host()
clock_getres()영향 없음 (정밀도는 동일)없음
/proc/uptime오프셋 적용된 monotonic 표시proc_uptime_show()
/proc/timer_list호스트 시간 기준 (오프셋 미적용)없음
절대 시간 역변환: clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, ...)처럼 절대 시간을 지정하는 경우, 커널은 timens_ktime_to_host()를 통해 namespace 시간을 호스트 시간으로 역변환합니다. 이렇게 해야 올바른 시점에 타이머가 만료됩니다.

vDSO 통합

vDSO 문제점

clock_gettime()은 성능을 위해 vDSO(Virtual Dynamic Shared Object)를 통해 userspace에서 직접 실행됩니다. 시스템 콜을 거치지 않으므로, time namespace 오프셋도 vDSO 코드가 직접 접근할 수 있어야 합니다. 이를 해결하기 위해 namespace별 별도의 VVAR 페이지를 사용합니다.

Process A (init time namespace) vDSO 코드 (공유) VVAR 페이지 (init) offset = 0, 0 clock_gettime(MONOTONIC) = 1000s Process B (custom time namespace) vDSO 코드 (공유) VVAR 페이지 (NS) offset = +5000, +5000 clock_gettime(MONOTONIC) = 6000s Kernel: 실제 monotonic = 1000s init VVAR: offset=(0,0) 결과: 1000 + 0 = 1000s NS VVAR: offset=(5000,0) 결과: 1000 + 5000 = 6000s

VVAR 페이지 분리 메커니즘

/* vDSO 내부에서 시간 읽기 (간략화) */
static int __vdso_clock_gettime_common(
    const struct vdso_data *vd,
    clockid_t clk, struct __kernel_timespec *ts)
{
    /* vd는 프로세스별 매핑된 VVAR 페이지를 가리킴
     * init NS: 글로벌 vdso_data
     * custom NS: namespace별 vdso_data (오프셋 포함)
     */
    switch (clk) {
    case CLOCK_MONOTONIC:
        /* VVAR 데이터에서 직접 읽기 (이미 오프셋 반영됨) */
        do_monotonic(vd, ts);
        break;
    case CLOCK_BOOTTIME:
        do_boottime(vd, ts);
        break;
    ...
    }
}

/* 커널 측: VVAR 페이지 업데이트 */
static void timens_setup_vvar_page(struct time_namespace *ns)
{
    struct vdso_data *vdata;

    /* namespace별 VVAR 페이지 할당 */
    ns->vvar_page = alloc_page(GFP_KERNEL_ACCOUNT | __GFP_ZERO);

    /* 오프셋 반영 데이터 복사 */
    vdata = page_address(ns->vvar_page);
    timens_update_vvar(vdata, ns);
}

vDSO 성능 영향

항목init time NScustom time NS차이
clock_gettime() 경로vDSO (글로벌 VVAR)vDSO (NS VVAR)동일
메모리 오버헤드없음+1 page (4KB) per NS미미
fork() 비용기본VVAR 페이지 재매핑약간 증가
context switch기본기본 (VVAR은 프로세스별)없음
호출 속도약 20~30ns약 20~30ns차이 없음
성능 참고: vDSO 경로를 사용하므로 time namespace가 활성화되어도 clock_gettime() 호출 성능에는 실질적인 차이가 없습니다. 추가 비용은 namespace 생성 시 VVAR 페이지 할당(1회)과 fork 시 재매핑뿐입니다.

아키텍처별 vDSO 구현

vDSO의 time namespace 지원은 아키텍처별로 다르게 구현됩니다. x86_64에서는 arch/x86/entry/vdso/에, ARM64에서는 arch/arm64/kernel/vdso/에 위치합니다.

/* x86_64 vDSO: __vdso_clock_gettime() 구현 (간략화)
 * arch/x86/entry/vdso/vclock_gettime.c */

notrace int __vdso_clock_gettime(
    clockid_t clock,
    struct __kernel_timespec *ts)
{
    /* VVAR 페이지에서 vdso_data 가져오기
     * init NS: 글로벌 vdso_data 직접 참조
     * custom NS: NS별 매핑된 VVAR 페이지 참조 */
    const struct vdso_data *vd = __arch_get_vdso_data();

    /* 시계 종류에 따라 오프셋이 이미 반영된 데이터 읽기 */
    return __cvdso_clock_gettime_common(vd, clock, ts);
}

/* VVAR 페이지 매핑 — arch/x86/entry/vdso/vma.c */
static vm_fault_t vvar_fault(const struct vm_special_mapping *sm,
                            struct vm_area_struct *vma,
                            struct vm_fault *vmf)
{
    struct page *timens_page =
        find_timens_vvar_page(vma);

    if (timens_page) {
        /* Custom time NS: NS별 VVAR 페이지 사용 */
        vmf->page = timens_page;
    } else {
        /* Init time NS: 글로벌 VVAR 페이지 사용 */
        vmf->page = virt_to_page(&vdso_data);
    }

    get_page(vmf->page);
    return 0;
}

VVAR 페이지 업데이트 메커니즘

커널의 timekeeping 시스템이 시간을 갱신할 때, NS별 VVAR 페이지도 함께 갱신해야 합니다. 이 과정은 update_vsyscall()에서 처리됩니다.

/* timekeeping 갱신 시 VVAR 페이지 업데이트
 * kernel/time/vsyscall.c */
void update_vsyscall(struct timekeeper *tk)
{
    struct vdso_data *vdata = __arch_get_k_vdso_data();

    /* 글로벌 VVAR 데이터 갱신 (init NS) */
    vdso_write_begin(vdata);
    vdata[CS_HRES_COARSE].basetime[CLOCK_MONOTONIC] =
        tk_to_timespec64(tk, CLOCK_MONOTONIC);
    vdata[CS_HRES_COARSE].basetime[CLOCK_BOOTTIME] =
        tk_to_timespec64(tk, CLOCK_BOOTTIME);
    vdso_write_end(vdata);

    /* 각 NS별 VVAR 페이지 업데이트
     * timens_commit()에서 설정한 NS 페이지에
     * 오프셋이 반영된 값을 기록 */
    update_timens_vvar();
}

/* NS별 VVAR 데이터에 오프셋 반영 */
static void timens_setup_vvar_data(
    struct vdso_data *vdata,
    struct time_namespace *ns)
{
    /* 글로벌 데이터 복사 */
    memcpy(vdata, __arch_get_k_vdso_data(),
           sizeof(*vdata) * CS_BASES);

    /* MONOTONIC 계열: monotonic 오프셋 적용 */
    vdata[CS_HRES_COARSE].basetime[CLOCK_MONOTONIC] =
        timespec64_add(
            vdata[CS_HRES_COARSE].basetime[CLOCK_MONOTONIC],
            ns->offsets.monotonic);

    /* BOOTTIME 계열: boottime 오프셋 적용 */
    vdata[CS_HRES_COARSE].basetime[CLOCK_BOOTTIME] =
        timespec64_add(
            vdata[CS_HRES_COARSE].basetime[CLOCK_BOOTTIME],
            ns->offsets.boottime);
}
Hardware Clock TSC / HPET / ACPI PM Timekeeping Core update_wall_time() update_vsyscall() VVAR 데이터 갱신 글로벌 VVAR (init NS) MONOTONIC = 1000s BOOTTIME = 1000s NS VVAR (offset=+5000) MONOTONIC = 6000s (+5000) BOOTTIME = 6000s (+5000) Process A: clock_gettime() = 1000s vDSO → init VVAR 참조 Process B: clock_gettime() = 6000s vDSO → NS VVAR 참조
vDSO seqcount: VVAR 페이지 업데이트 중 userspace가 데이터를 읽으면 불일치가 발생할 수 있습니다. 이를 방지하기 위해 seqcount 기반 동기화를 사용합니다. vDSO 코드는 vdso_read_begin()/vdso_read_retry()로 읽기 중 갱신이 발생했는지 감지하고, 발생했으면 재시도합니다. 이 메커니즘은 lock-free이므로 성능 영향이 없습니다.

CRIU 통합

CRIU (Checkpoint/Restore In Userspace)

CRIU는 실행 중인 프로세스/컨테이너를 디스크에 저장(checkpoint)하고 다른 호스트에서 복원(restore)하는 도구입니다. Time namespace는 CRIU의 핵심 요구사항인 시간 연속성을 충족하기 위해 설계되었습니다.

time Host A 시작 monotonic = 100s boottime = 100s Checkpoint monotonic = 500s 저장: uptime=500s 전송 Host B 복원 호스트 monotonic = 8000s 오프셋 계산 mono_offset = 500 - 8000 = -7500s boot_offset = 500 - 8000 = -7500s 복원된 컨테이너: clock_gettime(MONOTONIC) 8000 + (-7500) = 500s (원래 값 유지)

CRIU 복원 흐름

/* CRIU 복원 시 time namespace 설정 순서 (개념 코드) */

/* 1. 새 time namespace 생성 */
unshare(CLONE_NEWTIME);

/* 2. 체크포인트 이미지에서 원본 시간 읽기 */
time64_t saved_monotonic = 500;  /* checkpoint 시점의 monotonic */
time64_t saved_boottime  = 500;  /* checkpoint 시점의 boottime */

/* 3. 현재 호스트 시간 확인 */
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);  /* 호스트: 8000s */

/* 4. 오프셋 계산 및 설정 */
int fd = open("/proc/self/timens_offsets", O_WRONLY);
char buf[128];
snprintf(buf, sizeof(buf), "monotonic %lld 0\n",
         saved_monotonic - now.tv_sec);  /* 500 - 8000 = -7500 */
write(fd, buf, strlen(buf));

snprintf(buf, sizeof(buf), "boottime %lld 0\n",
         saved_boottime - now.tv_sec);
write(fd, buf, strlen(buf));
close(fd);

/* 5. 복원 프로세스 생성 (새 time NS에 진입) */
pid_t child = fork();
if (child == 0) {
    /* 자식: clock_gettime(MONOTONIC) = 500s (원래 값) */
    restore_process_state(...);
}

Checkpoint/Restore 시나리오 상세

# 1. Source Host에서 컨테이너 실행 중
$ docker exec container1 cat /proc/uptime
500.123 1800.456

# 2. Checkpoint
$ sudo criu dump -t $(docker inspect -f '{{.State.Pid}}' container1) \
    -D /tmp/checkpoint --shell-job --tcp-established

# 3. 이미지 전송
$ rsync -avz /tmp/checkpoint/ dest-host:/tmp/checkpoint/

# 4. Destination Host에서 복원
$ sudo criu restore -D /tmp/checkpoint --shell-job --tcp-established

# 5. 복원 확인 -- uptime이 연속됨
$ docker exec container1 cat /proc/uptime
502.789 1802.345   # checkpoint 시점 + 경과 시간
CRIU 호환성: CRIU 3.15 이상에서 time namespace를 완전히 지원합니다. --time-namespace 옵션으로 명시적으로 활성화할 수 있으며, 최신 버전에서는 자동 감지합니다.

CRIU 라이브 마이그레이션 연동

라이브 마이그레이션에서는 다운타임을 최소화하면서 프로세스/컨테이너를 다른 호스트로 이전합니다. Time namespace는 이 과정에서 시간 연속성을 보장하는 핵심 역할을 합니다.

Source Host (Host A) 1. Pre-dump: 메모리 페이지 사전 복사 (반복) 2. Freeze: 프로세스 중단 (SIGSTOP) 3. Dump: 상태 저장 monotonic=500s, boottime=500s 기록 4. 더티 페이지 + 이미지 전송 Dest Host (Host B) 5. 이미지 수신, 메모리 복원 준비 6. Time NS 생성 + 오프셋 설정 호스트 monotonic=8000s offset = 500 - 8000 = -7500s 7. fork() → 프로세스 복원 (NS 진입) 8. 프로세스 재개: monotonic = 500s (연속) rsync/전송 다운타임: Step 2~8 구간 일반적으로 수십 ms ~ 수 초

CRIU 오프셋 계산 알고리즘

/* CRIU 복원 시 time namespace 오프셋 계산 알고리즘 */

struct criu_timens_data {
    struct timespec64 saved_monotonic;  /* checkpoint 시 값 */
    struct timespec64 saved_boottime;   /* checkpoint 시 값 */
    time64_t          transfer_delay;   /* 전송 지연 보정 */
};

int criu_setup_timens(struct criu_timens_data *data)
{
    struct timespec64 host_mono, host_boot;
    int64_t mono_offset, boot_offset;

    /* 1. 현재 호스트 시간 측정 */
    clock_gettime(CLOCK_MONOTONIC, &host_mono);
    clock_gettime(CLOCK_BOOTTIME, &host_boot);

    /* 2. 전송 지연 보정 포함 오프셋 계산 */
    mono_offset = (data->saved_monotonic.tv_sec +
                   data->transfer_delay) -
                  host_mono.tv_sec;

    boot_offset = (data->saved_boottime.tv_sec +
                   data->transfer_delay) -
                  host_boot.tv_sec;

    /* 3. unshare(CLONE_NEWTIME) */
    unshare(CLONE_NEWTIME);

    /* 4. 오프셋 설정 */
    int fd = open("/proc/self/timens_offsets", O_WRONLY);
    dprintf(fd, "monotonic %lld %ld\n",
            mono_offset, data->saved_monotonic.tv_nsec);
    dprintf(fd, "boottime %lld %ld\n",
            boot_offset, data->saved_boottime.tv_nsec);
    close(fd);

    /* 5. fork() 후 자식에서 프로세스 복원 */
    return 0;
}

/* 주의: 전송 지연(transfer_delay)은 pre-dump 기간과
 * 최종 dump~restore 사이의 시간을 포함해야 합니다.
 * 이 값이 정확하지 않으면 복원된 프로세스의 타이머가
 * 약간 빠르거나 느리게 동작할 수 있습니다. */
라이브 마이그레이션 주의사항:
  • CLOCK_REALTIME 불일치: 소스와 대상 호스트의 wall clock이 NTP로 동기화되어 있어야 합니다
  • 전송 지연 보정: dump~restore 사이의 시간을 오프셋에 반영해야 타이머가 정확합니다
  • 다운타임 최소화: pre-dump으로 메모리를 사전 복사하고, 최종 dump는 dirty 페이지만 전송
  • TCP 연결 유지: --tcp-established 옵션으로 TCP 상태 보존 시 시간 연속성이 중요

futex/timerfd/nanosleep 동작

timerfd와 time namespace

timerfd는 MONOTONIC/BOOTTIME 시계를 사용하는 파일 디스크립터 기반 타이머입니다. Time namespace에서 timerfd가 올바르게 동작하려면, 절대 시간 지정 시 오프셋 역변환이 필요합니다.

#include <sys/timerfd.h>

/* timerfd + time namespace 동작 */
int tfd = timerfd_create(CLOCK_MONOTONIC, 0);

/* 상대 시간: 오프셋 영향 없음 (5초 후) */
struct itimerspec rel = {
    .it_value = { .tv_sec = 5, .tv_nsec = 0 },
};
timerfd_settime(tfd, 0, &rel, NULL);
/* 정확히 5초 후 만료 (모든 NS에서 동일) */

/* 절대 시간: 오프셋 역변환 적용 */
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now); /* NS 시간: 6000s (호스트: 1000s, offset: +5000) */

struct itimerspec abs = {
    .it_value = { .tv_sec = now.tv_sec + 10, .tv_nsec = 0 },
    /* NS 시간 6010s 에 만료하고 싶음
     * 커널은 이를 호스트 시간 1010s로 역변환 (6010 - 5000 = 1010)
     * 실제로 호스트 monotonic이 1010에 도달하면 만료 */
};
timerfd_settime(tfd, TFD_TIMER_ABSTIME, &abs, NULL);
Namespace 관점 (offset=+5000s) timerfd_settime(ABSTIME, 6010s) 현재: 6000s, 10초 후 만료 기대 timens_ktime_to_host() 6010 - 5000 = 1010s (호스트 시간으로 변환) Host 관점 hrtimer expires = 1010s 현재 1000s, 10초 후 만료 (일치)

futex와 time namespace

FUTEX_WAIT_BITSETFUTEX_CLOCK_REALTIME이 아닌 MONOTONIC 기반 절대 타임아웃을 사용하면 time namespace 오프셋이 적용됩니다.

/* futex의 time namespace 영향 */

/* 상대 타임아웃: 영향 없음 */
struct timespec timeout = { .tv_sec = 5, .tv_nsec = 0 };
futex(addr, FUTEX_WAIT, val, &timeout, NULL, 0);
/* 정확히 5초 대기 */

/* 절대 타임아웃 (CLOCK_MONOTONIC): 오프셋 적용 */
struct timespec abs_time;
clock_gettime(CLOCK_MONOTONIC, &abs_time);
abs_time.tv_sec += 5;
futex(addr, FUTEX_WAIT_BITSET, val, &abs_time, NULL, FUTEX_BITSET_MATCH_ANY);
/* 커널이 절대 시간을 호스트 시간으로 역변환 후 대기 */

nanosleep/clock_nanosleep

함수모드Time NS 영향
nanosleep()상대 시간만영향 없음
clock_nanosleep(MONOTONIC, 0, ...)상대영향 없음
clock_nanosleep(MONOTONIC, TIMER_ABSTIME, ...)절대오프셋 역변환 적용
clock_nanosleep(BOOTTIME, TIMER_ABSTIME, ...)절대오프셋 역변환 적용
clock_nanosleep(REALTIME, TIMER_ABSTIME, ...)절대영향 없음
주의: 상대 시간 대기(sleep(5), nanosleep({5,0}))는 time namespace의 영향을 받지 않습니다. 오프셋은 오직 절대 시간 기반 타이머/대기에만 역변환이 적용됩니다.

POSIX 타이머와 time namespace

timer_create()로 생성한 POSIX 타이머도 time namespace의 영향을 받습니다. 절대 시간 지정 시 오프셋 역변환이 적용되며, 커널 내부에서 timens_ktime_to_host()가 호출됩니다.

/* POSIX 타이머 + time namespace 동작 예시 */
#include <signal.h>
#include <time.h>

timer_t timerid;
struct sigevent sev = {
    .sigev_notify = SIGEV_SIGNAL,
    .sigev_signo = SIGALRM,
};

/* MONOTONIC 기반 타이머 생성 */
timer_create(CLOCK_MONOTONIC, &sev, &timerid);

/* 상대 시간: 오프셋 영향 없음 */
struct itimerspec its_rel = {
    .it_value = { .tv_sec = 10, .tv_nsec = 0 },
    .it_interval = { .tv_sec = 1, .tv_nsec = 0 },
};
timer_settime(timerid, 0, &its_rel, NULL);
/* 정확히 10초 후 첫 번째 만료, 이후 1초 간격 */

/* 절대 시간: 오프셋 역변환 적용 */
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);

struct itimerspec its_abs = {
    .it_value = {
        .tv_sec = now.tv_sec + 30,  /* NS 기준 30초 후 */
        .tv_nsec = 0,
    },
};
timer_settime(timerid, TIMER_ABSTIME, &its_abs, NULL);
/* 커널이 NS 시간을 호스트 시간으로 역변환 후
 * 호스트 MONOTONIC 기준으로 hrtimer 설정 */

타이머 커널 경로 분석

/* do_timer_settime() 내 time namespace 처리
 * kernel/time/posix-timers.c (간략화) */
static int do_timer_settime(timer_t timer_id,
                           int tmr_flags,
                           struct itimerspec64 *new_spec64,
                           struct itimerspec64 *old_spec64)
{
    struct k_itimer *timr;

    timr = lock_timer(timer_id, &flags);

    /* 절대 시간 + MONOTONIC/BOOTTIME이면 오프셋 역변환 */
    if (tmr_flags & TIMER_ABSTIME) {
        ktime_t exp = timespec64_to_ktime(new_spec64->it_value);
        /* NS 시간 → 호스트 시간 변환 */
        exp = timens_ktime_to_host(timr->it_clock, exp);
        new_spec64->it_value = ktime_to_timespec64(exp);
    }

    /* hrtimer에 호스트 시간 기준으로 설정 */
    common_timer_set(timr, tmr_flags, new_spec64);

    return 0;
}

futex2와 time namespace

Linux 5.16에서 도입된 futex_waitv() 시스템 콜도 time namespace를 지원합니다. 절대 타임아웃 지정 시 오프셋 역변환이 적용됩니다.

/* futex_waitv + time namespace 동작 */
struct futex_waitv waiters[2] = {
    { .val = 0, .uaddr = (__u64)&futex1,
      .flags = FUTEX_32 | FUTEX_PRIVATE_FLAG },
    { .val = 0, .uaddr = (__u64)&futex2,
      .flags = FUTEX_32 | FUTEX_PRIVATE_FLAG },
};

struct timespec timeout;
clock_gettime(CLOCK_MONOTONIC, &timeout);
timeout.tv_sec += 10;  /* NS 시간 기준 10초 후 */

/* futex_waitv는 절대 시간 지정 시
 * 커널 내부에서 timens_ktime_to_host() 호출 */
syscall(SYS_futex_waitv, waiters, 2,
        0, &timeout, CLOCK_MONOTONIC);
time namespace 영향 요약:
  • 영향받는 것: clock_gettime(MONOTONIC/BOOTTIME), 절대 시간 지정 타이머/sleep, /proc/uptime
  • 영향받지 않는 것: 상대 시간 sleep, CLOCK_REALTIME, CLOCK_PROCESS_CPUTIME_ID, CPU 시간, perf 타임스탬프
  • 역변환 필요: 절대 시간 지정 시에만 (TIMER_ABSTIME, TFD_TIMER_ABSTIME)

시간 네임스페이스와 컨테이너 런타임

컨테이너 런타임별 지원 현황

런타임/도구Time NS 지원버전설정 방법
runc지원1.1+OCI spec linux.namespaces
crun지원1.0+OCI spec (C 구현)
containerd부분 지원1.6+OCI spec 전달
Docker미지원 (기본)-CRIU checkpoint만
Podman부분 지원4.0+OCI spec 직접 작성
Kubernetes실험적1.25+CRI + checkpoint
LXC/LXD지원4.0+설정 파일
systemd-nspawn미지원--

OCI Runtime Spec 통합

/* OCI Runtime Spec에서 time namespace 설정
 * config.json (JSON 형식) */
# OCI config.json 예시 (time namespace 포함)
# {
#   "linux": {
#     "namespaces": [
#       { "type": "pid" },
#       { "type": "network" },
#       { "type": "mount" },
#       { "type": "ipc" },
#       { "type": "uts" },
#       { "type": "cgroup" },
#       { "type": "time" }
#     ],
#     "timeOffsets": {
#       "monotonic": { "secs": 1000, "nanosecs": 0 },
#       "boottime":  { "secs": 2000, "nanosecs": 0 }
#     }
#   }
# }

# runc에서 직접 실행
$ sudo runc create --bundle /path/to/bundle container1
$ sudo runc start container1

# 컨테이너 내부에서 확인
$ runc exec container1 cat /proc/uptime
1000.123 ...  # monotonic offset 반영

LXC/LXD에서 time namespace

# LXC 설정 파일에서 time namespace 활성화
# /var/lib/lxc/mycontainer/config
lxc.namespace.time = ""

# LXD에서 time namespace 사용
$ lxc config set mycontainer raw.lxc "lxc.namespace.time ="
$ lxc restart mycontainer

# 확인
$ lxc exec mycontainer -- cat /proc/uptime
$ lxc exec mycontainer -- readlink /proc/self/ns/time

Kubernetes Checkpoint/Restore

Kubernetes 1.25부터 컨테이너 체크포인트 기능이 알파로 도입되었습니다. CRI (Container Runtime Interface)를 통해 CRIU를 호출하며, time namespace 오프셋은 자동으로 처리됩니다.

# Kubernetes 컨테이너 체크포인트 (알파 기능, 1.25+)
# Feature gate 활성화 필요: ContainerCheckpoint=true

# 1. 체크포인트 생성
$ kubectl checkpoint pod/myapp --container=main \
    --timeout=30s

# 2. 체크포인트 이미지 확인
$ ls /var/lib/kubelet/checkpoints/
checkpoint-myapp-main-2024-01-15T10:30:00Z.tar

# 3. 다른 노드에서 복원 (Pod 생성 시 지정)
# apiVersion: v1
# kind: Pod
# metadata:
#   annotations:
#     io.kubernetes.cri.checkpoint-restore: "true"
# spec:
#   containers:
#   - name: main
#     image: checkpoint-myapp-main:latest

# crictl을 통한 직접 체크포인트 (CRI 레벨)
$ crictl checkpoint --export=/tmp/cp.tar $CONTAINER_ID
$ crictl create --restore=/tmp/cp.tar $POD_ID config.json
운영 팁: 컨테이너 런타임에서 time namespace를 사용할 때:
  • CRIU 기반 체크포인트/복원에서는 time namespace가 자동으로 설정됩니다
  • 수동으로 오프셋을 설정하려면 OCI spec의 timeOffsets 필드를 사용합니다
  • 모니터링 에이전트가 컨테이너 내부에서 실행되면 MONOTONIC 기반 지표가 오프셋 적용됩니다
  • Prometheus node_boot_time_seconds는 time namespace 영향을 받으므로 주의

예제 코드

Time Namespace 생성

#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/wait.h>

void print_clocks(const char *label)
{
    struct timespec mono, boot;
    clock_gettime(CLOCK_MONOTONIC, &mono);
    clock_gettime(CLOCK_BOOTTIME, &boot);
    printf("[%s] MONOTONIC=%ld.%09ld  BOOTTIME=%ld.%09ld\n",
           label, mono.tv_sec, mono.tv_nsec,
           boot.tv_sec, boot.tv_nsec);
}

int main(void)
{
    print_clocks("Parent before");

    /* 새 Time Namespace 생성 */
    if (unshare(CLONE_NEWTIME) == -1) {
        perror("unshare(CLONE_NEWTIME)");
        return 1;
    }

    /* 오프셋 설정: MONOTONIC +1000초, BOOTTIME +2000초 */
    int fd = open("/proc/self/timens_offsets", O_WRONLY);
    if (fd < 0) { perror("open"); return 1; }

    dprintf(fd, "monotonic %d %d\n", 1000, 0);
    dprintf(fd, "boottime %d %d\n", 2000, 0);
    close(fd);

    print_clocks("Parent after offset");
    /* 부모는 여전히 기존 NS: 값 변화 없음 */

    pid_t pid = fork();
    if (pid == -1) { perror("fork"); return 1; }

    if (pid == 0) {
        /* 자식: 새 time NS에 진입 */
        print_clocks("Child (new NS)");
        /* MONOTONIC +1000s, BOOTTIME +2000s 적용됨 */
        _exit(0);
    }

    waitpid(pid, NULL, 0);
    print_clocks("Parent after child");
    return 0;
}

/* 예상 출력:
[Parent before]      MONOTONIC=1234.000000000  BOOTTIME=1234.000000000
[Parent after offset] MONOTONIC=1234.000100000  BOOTTIME=1234.000100000
[Child (new NS)]     MONOTONIC=2234.000200000  BOOTTIME=3234.000200000
[Parent after child] MONOTONIC=1234.000300000  BOOTTIME=1234.000300000
*/

nsenter/unshare 명령행 예제

# 호스트 현재 시간 확인
$ cat /proc/uptime
12345.67 48000.89

# Time namespace 생성 + 오프셋 설정 + 진입
$ sudo unshare --time --fork --map-current-user /bin/bash -c '
  echo "monotonic 86400 0" > /proc/self/timens_offsets
  echo "boottime 86400 0" >> /proc/self/timens_offsets
  exec /bin/bash
'

# 새 namespace에서 확인 (+86400초 = +1일)
$ cat /proc/uptime
98745.67 ...

# namespace ID 확인
$ ls -l /proc/self/ns/time
lrwxrwxrwx ... /proc/self/ns/time -> 'time:[4026532XXX]'

# 다른 터미널에서 nsenter로 같은 namespace 진입
$ sudo nsenter -T -t $PID /bin/bash
$ cat /proc/uptime    # 같은 오프셋 적용

Python에서 Time Namespace 활용

#!/usr/bin/env python3
"""time_namespace.py -- Python에서 time namespace 생성 및 검증"""

import ctypes
import os
import time
import struct

# libc 함수 바인딩
libc = ctypes.CDLL("libc.so.6", use_errno=True)

# clone(2) 플래그
CLONE_NEWTIME = 0x00000080

# unshare(2) 호출
def unshare(flags):
    ret = libc.unshare(flags)
    if ret != 0:
        errno = ctypes.get_errno()
        raise OSError(errno, os.strerror(errno))

def get_monotonic():
    return time.clock_gettime(time.CLOCK_MONOTONIC)

def get_boottime():
    return time.clock_gettime(time.CLOCK_BOOTTIME)

def main():
    print(f"Before unshare:")
    print(f"  MONOTONIC = {get_monotonic():.9f}")
    print(f"  BOOTTIME  = {get_boottime():.9f}")

    # 새 time namespace 생성 (현재 프로세스는 영향 없음)
    unshare(CLONE_NEWTIME)

    # 오프셋 설정: +1000초 monotonic, +2000초 boottime
    with open("/proc/self/timens_offsets", "w") as f:
        f.write("monotonic 1000 0\n")
        f.write("boottime 2000 0\n")

    # fork()로 자식이 새 NS에 진입
    pid = os.fork()
    if pid == 0:
        # 자식: 새 time namespace
        print(f"Child (new NS):")
        print(f"  MONOTONIC = {get_monotonic():.9f}  (+1000s)")
        print(f"  BOOTTIME  = {get_boottime():.9f}  (+2000s)")

        # 오프셋 확인
        with open("/proc/self/timens_offsets") as f:
            print(f"  Offsets: {f.read().strip()}")
        os._exit(0)
    else:
        os.waitpid(pid, 0)
        print(f"Parent (still init NS):")
        print(f"  MONOTONIC = {get_monotonic():.9f}")

if __name__ == "__main__":
    main()

Go에서 Time Namespace 활용

// timens_example.go -- Go에서 time namespace 생성
package main

import (
    "fmt"
    "os"
    "os/exec"
    "syscall"
    "time"
)

const CLONE_NEWTIME = 0x00000080

func getMonotonic() time.Duration {
    var ts syscall.Timespec
    syscall.ClockGettime(syscall.CLOCK_MONOTONIC, &ts)
    return time.Duration(ts.Sec)*time.Second +
           time.Duration(ts.Nsec)*time.Nanosecond
}

func main() {
    if os.Getenv("IN_TIMENS") == "1" {
        // 자식 프로세스: 새 time NS 내부에서 실행 중
        fmt.Printf("Child MONOTONIC: %v\n", getMonotonic())
        return
    }

    fmt.Printf("Parent MONOTONIC: %v\n", getMonotonic())

    // 자식 프로세스를 새 time namespace에서 실행
    cmd := exec.Command("/proc/self/exe")
    cmd.Env = append(os.Environ(), "IN_TIMENS=1")
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags: CLONE_NEWTIME,
    }

    // 오프셋은 /proc/[child-pid]/timens_offsets에 설정해야 함
    // SysProcAttr에서는 직접 지원하지 않으므로
    // 별도 wrapper 스크립트나 helper 프로세스 필요

    if err := cmd.Run(); err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        os.Exit(1)
    }
}
Go 참고: Go의 syscall.SysProcAttr.CloneflagsCLONE_NEWTIME을 설정하면 자식 프로세스가 새 time namespace에서 시작됩니다. 다만 오프셋 설정은 /proc/[pid]/timens_offsets에 직접 쓰기가 필요하며, 이는 부모 프로세스에서 자식 PID를 알아야 하므로 helper 프로세스 패턴이 일반적입니다.

커널 설정

# Time Namespace 필수
CONFIG_TIME_NS=y           # Time Namespace 지원 (Linux 5.6+)

# 관련 namespace (일반적으로 함께 활성화)
CONFIG_UTS_NS=y
CONFIG_IPC_NS=y
CONFIG_PID_NS=y
CONFIG_NET_NS=y
CONFIG_USER_NS=y
CONFIG_CGROUP_NS=y
CONFIG_MNT_NS=y

# vDSO 지원 (time NS에 필수)
CONFIG_GENERIC_VDSO_TIME_NS=y  # 아키텍처별 vDSO time NS 지원 */

아키텍처별 지원 현황

아키텍처Time NS 지원커널 버전비고
x86_64지원5.6+최초 구현
ARM64 (aarch64)지원5.7+vDSO 연동 완료
ARM (32-bit)지원5.11+vDSO 제한적
RISC-V지원5.12+
s390지원5.7+
PowerPC지원5.11+
MIPS미지원-vDSO 미구현

procfs/sysfs 인터페이스

경로읽기/쓰기설명
/proc/[pid]/ns/timeRTime namespace 심볼릭 링크 (nsenter 대상)
/proc/[pid]/ns/time_for_childrenR자식용 time namespace 심볼릭 링크
/proc/[pid]/timens_offsetsR/W오프셋 설정/확인 (frozen 전에만 쓰기 가능)
/proc/uptimeR오프셋 적용된 monotonic 시간
/proc/timer_listR호스트 시간 기준 (오프셋 미적용)
# namespace ID 확인
$ ls -l /proc/self/ns/time
lrwxrwxrwx 1 user user 0 ... /proc/self/ns/time -> 'time:[4026531834]'

# 자식용 namespace 확인 (unshare 후 달라짐)
$ ls -l /proc/self/ns/time_for_children

# 오프셋 확인
$ cat /proc/self/timens_offsets
monotonic           0         0
boottime            0         0

# 특정 PID의 time NS 확인
$ readlink /proc/1234/ns/time
time:[4026532001]

제약사항

CLOCK_REALTIME 격리 불가

CLOCK_REALTIME은 time namespace의 영향을 받지 않습니다. 이는 의도된 설계입니다.

격리 불가 이유설명
파일 타임스탬프파일 생성/수정 시각은 전역 일관성 필요
TLS/SSL 인증서인증서 유효기간 검증이 실패할 수 있음
네트워크 프로토콜NTP, Kerberos, DNSSEC 등이 wall clock 의존
로그 일관성호스트/컨테이너 간 로그 시간대가 달라지면 분석 곤란
CRIU 불필요CRIU는 monotonic만 복원하면 됨 (realtime은 NTP로 동기화)

VDSO 페이지 전환 제약

실행 중인 프로세스의 VDSO 페이지 매핑을 안전하게 교체할 수 없기 때문에, setns()로 다른 time namespace에 직접 진입하는 것은 불가능합니다. 반드시 fork()를 통해 자식 프로세스가 진입해야 합니다.

/* setns()로 time NS 직접 진입 시도 -- 동작하지 않음 */
int ns_fd = open("/proc/1234/ns/time", O_RDONLY);
int ret = setns(ns_fd, CLONE_NEWTIME);
/* ret == -1, errno == EINVAL
 * setns()는 time_ns_for_children만 변경
 * 실제 전환은 fork() 후 자식에서 발생 */

Frozen Offset

# 오프셋 동결 후 쓰기 시도
$ echo "monotonic 2000 0" > /proc/self/timens_offsets
bash: /proc/self/timens_offsets: Permission denied
# errno: EACCES (frozen_offsets == true)
위험: 오프셋을 잘못 설정하면 컨테이너 내의 모든 타이머/타임아웃 동작이 망가질 수 있습니다. 특히 음수 오프셋이 현재 monotonic 값보다 크면(결과가 음수) clock_gettime()이 0을 반환하는 등 예측 불가능한 동작이 발생합니다.

운영 패턴

컨테이너별 독립 uptime

#!/bin/bash
# 컨테이너별 독립 uptime 구현

# 현재 호스트 monotonic 확인
HOST_MONO=$(awk '{print int($1)}' /proc/uptime)

# 컨테이너에서 0부터 시작하는 uptime 제공
OFFSET="-${HOST_MONO}"

sudo unshare --time --fork /bin/bash -c "
  echo 'monotonic ${OFFSET} 0' > /proc/self/timens_offsets
  echo 'boottime ${OFFSET} 0' >> /proc/self/timens_offsets
  exec /bin/bash
"

# 컨테이너 내부: /proc/uptime = 0 부터 시작

모니터링 영향

도구/지표Time NS 영향대응
Prometheus node_exporternode_boot_time_seconds 오프셋 적용됨호스트에서 수집 또는 레이블로 구분
uptime 명령오프셋 적용된 값 표시NS 내부 기준임을 인지
systemd 타이머MONOTONIC 기반 타이머 오프셋 적용REALTIME 기반 타이머는 영향 없음
perf/ftrace 타임스탬프호스트 기준 (영향 없음)NS 시간과 매핑 필요
dmesg 타임스탬프호스트 기준NS와 차이 있을 수 있음
운영 팁: 모니터링 에이전트가 컨테이너 내부에서 실행되면 CLOCK_MONOTONIC 기반 지표가 오프셋 적용된 값을 보고합니다. 호스트 레벨 모니터링에서는 영향이 없으므로, 가능하면 모니터링은 호스트 namespace에서 실행하는 것을 권장합니다.

CI/CD 파이프라인에서 시간 테스트

#!/bin/bash
# ci-time-test.sh -- CI 파이프라인에서 시간 의존 테스트 자동화
# Time namespace를 활용하여 시간 이동 테스트 수행

set -euo pipefail

# 테스트 매트릭스: 다양한 시간 오프셋으로 테스트
declare -a OFFSETS=(
    "0"             # 기본 (오프셋 없음)
    "86400"         # +1일
    "2592000"       # +30일
    "31536000"      # +1년
    "-3600"         # -1시간 (과거)
)

PASS=0
FAIL=0

for offset in "${OFFSETS[@]}"; do
    echo "=== Testing with monotonic offset: ${offset}s ==="

    if sudo unshare --time --fork /bin/bash -c "
        if [ '${offset}' != '0' ]; then
            echo 'monotonic ${offset} 0' > /proc/self/timens_offsets
            echo 'boottime ${offset} 0' >> /proc/self/timens_offsets
        fi
        exec ./run_tests.sh --time-offset=${offset}
    "; then
        echo "PASS: offset=${offset}"
        PASS=$((PASS + 1))
    else
        echo "FAIL: offset=${offset}"
        FAIL=$((FAIL + 1))
    fi
done

echo "Results: ${PASS} passed, ${FAIL} failed"
[ "${FAIL}" -eq 0 ]

멀티 테넌트 시간 관리

여러 컨테이너/테넌트가 각각 독립된 시간을 갖도록 구성하는 패턴입니다.

#!/bin/bash
# multi-tenant-time.sh -- 테넌트별 독립 시간 할당

create_tenant_ns() {
    local tenant_id="$1"
    local mono_offset="$2"
    local boot_offset="$3"

    echo "[${tenant_id}] Creating time NS: mono=${mono_offset}s, boot=${boot_offset}s"

    sudo unshare --time --fork /bin/bash -c "
        echo 'monotonic ${mono_offset} 0' > /proc/self/timens_offsets
        echo 'boottime ${boot_offset} 0' >> /proc/self/timens_offsets

        # PID 파일 생성 (다른 프로세스가 nsenter로 진입 가능)
        echo \$\$ > /run/tenant-${tenant_id}.pid

        # 테넌트 서비스 실행
        exec /usr/bin/tenant-service --id=${tenant_id}
    " &
}

# 테넌트별 시간 격리
# 테넌트 A: 10일 전 uptime 시뮬레이션 (-864000초)
create_tenant_ns "tenant-a" "-864000" "-864000"

# 테넌트 B: 기본 호스트 시간
create_tenant_ns "tenant-b" "0" "0"

# 테넌트 C: 미래 시간 (+30일)
create_tenant_ns "tenant-c" "2592000" "2592000"

wait

안전한 오프셋 계산 패턴

#define _GNU_SOURCE
#include <stdio.h>
#include <time.h>
#include <sched.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

/*
 * 안전한 오프셋 계산:
 * 결과 시간이 음수가 되지 않도록 검증
 */
int safe_set_timens_offset(
    long target_mono_sec,   /* 원하는 monotonic 시작값 */
    long target_boot_sec    /* 원하는 boottime 시작값 */
)
{
    struct timespec cur_mono, cur_boot;
    clock_gettime(CLOCK_MONOTONIC, &cur_mono);
    clock_gettime(CLOCK_BOOTTIME, &cur_boot);

    long mono_offset = target_mono_sec - cur_mono.tv_sec;
    long boot_offset = target_boot_sec - cur_boot.tv_sec;

    /* 결과가 음수가 되지 않도록 검증 */
    if (cur_mono.tv_sec + mono_offset < 0) {
        fprintf(stderr,
            "Warning: mono offset %ld would make time negative, clamping to -%ld\n",
            mono_offset, cur_mono.tv_sec);
        mono_offset = -cur_mono.tv_sec;
    }
    if (cur_boot.tv_sec + boot_offset < 0) {
        fprintf(stderr,
            "Warning: boot offset %ld would make time negative, clamping to -%ld\n",
            boot_offset, cur_boot.tv_sec);
        boot_offset = -cur_boot.tv_sec;
    }

    int fd = open("/proc/self/timens_offsets", O_WRONLY);
    if (fd < 0) {
        if (errno == EACCES)
            fprintf(stderr, "Error: offsets already frozen\n");
        return -1;
    }

    dprintf(fd, "monotonic %ld 0\n", mono_offset);
    dprintf(fd, "boottime %ld 0\n", boot_offset);
    close(fd);

    printf("Set offsets: mono=%+ld  boot=%+ld\n",
           mono_offset, boot_offset);
    return 0;
}
주의: 오프셋 계산 시 clock_gettime() 호출과 timens_offsets 쓰기 사이의 시간차를 고려해야 합니다. 정밀한 오프셋이 필요하면 나노초 단위까지 계산하고, 오프셋 적용 후 자식 프로세스에서 실제 값을 검증하세요.

성능 분석

clock_gettime() 성능 경로 비교 Fast Path (vDSO) ~15-25ns Userspace 호출 VVAR 페이지 읽기 오프셋 덧셈 (+3ns) 결과 반환 seqcount 검증 (재시도 ~0.01%) Slow Path (syscall) ~200-400ns Userspace 호출 syscall 진입 (context switch) do_clock_gettime() + 오프셋 적용 syscall 반환 커널 모드 전환 (~150-300ns 오버헤드) vDSO 경로는 syscall 대비 약 10-20배 빠름

vDSO 오프셋 적용 오버헤드

Time namespace가 활성화되면 clock_gettime()의 vDSO 경로에서 오프셋 덧셈이 추가됩니다. 이 연산의 오버헤드를 측정합니다.

시나리오clock_gettime() 평균 (ns)상대 오버헤드비고
init time NS (vDSO)~15-20기준VVAR 페이지 직접 읽기
custom time NS (vDSO)~18-25+15-25%오프셋 덧셈 + seqcount 추가
syscall fallback~200-400+1000%+vDSO 불가 시 커널 진입
CLOCK_REALTIME (vDSO)~15-20기준 (영향 없음)오프셋 적용 안 됨
/*
 * timens_vdso_bench.c -- vDSO clock_gettime 벤치마크
 * 컴파일: gcc -O2 -o timens_bench timens_vdso_bench.c
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <time.h>
#include <stdint.h>

#define ITERATIONS  10000000

static inline uint64_t rdtsc(void)
{
    uint32_t lo, hi;
    __asm__ volatile ("rdtsc" : "=a"(lo), "=d"(hi));
    return ((uint64_t)hi << 32) | lo;
}

int main(void)
{
    struct timespec ts;
    uint64_t start, end;
    int clocks[] = { CLOCK_MONOTONIC, CLOCK_MONOTONIC_RAW,
                     CLOCK_BOOTTIME, CLOCK_REALTIME };
    const char *names[] = { "MONOTONIC", "MONOTONIC_RAW",
                             "BOOTTIME",  "REALTIME" };

    for (int c = 0; c < 4; c++) {
        start = rdtsc();
        for (int i = 0; i < ITERATIONS; i++)
            clock_gettime(clocks[c], &ts);
        end = rdtsc();

        printf("%-18s  %8.1f cycles/call  (%llu total cycles)\n",
               names[c],
               (double)(end - start) / ITERATIONS,
               (unsigned long long)(end - start));
    }
    return 0;
}

메모리 오버헤드

리소스init NS (공유)custom NS (개별)설명
time_namespace 구조체전역 1개NS당 ~128Bkref, offsets, vvar_page 포인터
VVAR 페이지커널 전역 공유NS당 1-2 페이지 (4-8KB)별도 물리 페이지 할당
timens_offsets0B (offsets 없음)16B (2 x timespec64)monotonic + boottime
procfs 엔트리프로세스당 공유프로세스당 별도 inodetimens_offsets 파일
참고: 1000개의 독립 time namespace를 생성해도 추가 메모리 오버헤드는 약 8-16MB 수준입니다. 대부분의 비용은 VVAR 페이지 할당(NS당 4-8KB)이며, namespace 구조체 자체는 매우 작습니다.

확장성 테스트

#!/bin/bash
# timens_scalability.sh -- Time namespace 확장성 벤치마크

MAX_NS=500
echo "Creating ${MAX_NS} time namespaces..."

# 메모리 사용량 기준선
MEM_BEFORE=$(awk '/MemAvailable/ {print $2}' /proc/meminfo)

PIDS=()
for i in $(seq 1 $MAX_NS); do
    sudo unshare --time --fork /bin/bash -c "
        echo 'monotonic ${i}00 0' > /proc/self/timens_offsets
        echo 'boottime ${i}00 0' >> /proc/self/timens_offsets
        exec sleep 300
    " &
    PIDS+=($!)
done

sleep 2

MEM_AFTER=$(awk '/MemAvailable/ {print $2}' /proc/meminfo)
MEM_USED=$(( (MEM_BEFORE - MEM_AFTER) ))

echo "Created ${MAX_NS} namespaces"
echo "Memory used: ${MEM_USED} KB ($(( MEM_USED / MAX_NS )) KB/ns)"
echo "Active time namespaces:"
sudo lsns -t time | wc -l

# 정리
for pid in "${PIDS[@]}"; do kill $pid 2>/dev/null; done
wait

타이머 정확도 영향

Time namespace의 오프셋은 ktime_add()으로 적용되며, 타이머 정확도 자체에는 영향을 미치지 않습니다. 다만 VVAR seqcount 경쟁(contention) 시 읽기 재시도가 발생할 수 있습니다.

지표init NScustom NS차이 원인
clock_gettime 지터~5ns~5ns오프셋 덧셈은 결정적 연산
nanosleep 정확도±50us±50ushrtimer 해상도에 의존
timerfd 만료 지연~1-10us~1-10us커널 스케줄러에 의존
seqcount 재시도율~0.001%~0.01%VVAR 업데이트 빈도 차이

디버깅

Time Namespace 확인

# 프로세스의 time namespace 확인
$ ls -l /proc/$$/ns/time
lrwxrwxrwx 1 user user 0 ... /proc/12345/ns/time -> 'time:[4026531834]'

# init namespace인지 확인 (4026531834는 보통 init)
$ readlink /proc/1/ns/time   # init 프로세스의 time NS
time:[4026531834]

# 현재 프로세스가 init NS에 있는지 비교
$ if [ "$(readlink /proc/self/ns/time)" = "$(readlink /proc/1/ns/time)" ]; then
    echo "init time namespace"
  else
    echo "custom time namespace"
  fi

# 오프셋 확인
$ cat /proc/self/timens_offsets
monotonic       1000          0
boottime        2000  500000000

# 시간 비교 (호스트 vs NS)
$ nsenter -T -t 1 /usr/bin/cat /proc/uptime   # 호스트 시간
$ cat /proc/uptime                              # NS 시간

Tracing

# ftrace로 timens 관련 함수 추적
# echo 'timens_*' > /sys/kernel/debug/tracing/set_ftrace_filter
# echo function > /sys/kernel/debug/tracing/current_tracer
# echo 1 > /sys/kernel/debug/tracing/tracing_on

# bpftrace로 오프셋 적용 추적
$ sudo bpftrace -e '
kprobe:timens_add_monotonic {
    printf("pid=%d comm=%s\n", pid, comm);
}'

# strace로 clock_gettime 확인
$ strace -e trace=clock_gettime -p 1234 2>&1 | head
clock_gettime(CLOCK_MONOTONIC, {tv_sec=6234, tv_nsec=...}) = 0

디버깅 체크리스트

증상원인확인 방법
오프셋 쓰기 실패 (EACCES)frozen_offsets=true이미 프로세스가 NS에 진입
자식에서 시간 변화 없음오프셋 미설정 또는 0cat /proc/[pid]/timens_offsets
unshare 실패 (EPERM)CAP_SYS_ADMIN 없음root 또는 user NS 내에서 실행
uptime이 음수로 보임음수 오프셋이 현재 시간보다 큼오프셋 값 재계산
타이머가 즉시 만료절대 시간 지정 시 역변환 문제상대 시간 지정으로 변경
vDSO에서 잘못된 시간VVAR 페이지 매핑 오류/proc/[pid]/maps에서 vvar 확인
setns() 반환 EINVALtime NS에 직접 setns 불가fork() 후 자식에서 진입해야 함
nsenter -T 무반응대상 NS가 frozen 아님대상 PID가 실제 time NS에 있는지 확인
clock_nanosleep 오동작TIMER_ABSTIME + 오프셋 충돌상대 타이머 사용 또는 오프셋 조정

고급 bpftrace 추적

#!/usr/bin/env bpftrace
# timens_offset_trace.bt - Time namespace 오프셋 적용 상세 추적

# 오프셋 적용 추적 (monotonic)
$ sudo bpftrace -e '
kprobe:timens_add_monotonic {
    @mono[comm, pid] = count();
    printf("%-16s pid=%-6d applying monotonic offset\n", comm, pid);
}

kprobe:timens_add_boottime {
    @boot[comm, pid] = count();
    printf("%-16s pid=%-6d applying boottime offset\n", comm, pid);
}

kretprobe:timens_ktime_to_host {
    printf("%-16s pid=%-6d ns->host ktime conversion ret=%lld\n",
           comm, pid, retval);
}

END {
    printf("\n=== Monotonic offset calls ===\n");
    print(@mono);
    printf("\n=== Boottime offset calls ===\n");
    print(@boot);
}
'
# vDSO 경로 vs syscall 경로 비교 추적
$ sudo bpftrace -e '
tracepoint:syscalls:sys_enter_clock_gettime {
    @syscall_path[comm, pid, args->which_clock] = count();
}

kprobe:do_clock_gettime {
    @kernel_path[comm, pid] = count();
}

interval:s:5 {
    printf("\n=== syscall fallback (vDSO miss) ===\n");
    print(@syscall_path);
    printf("\n=== kernel path ===\n");
    print(@kernel_path);
    clear(@syscall_path);
    clear(@kernel_path);
}
'

perf 기반 성능 분석

# clock_gettime syscall 오버헤드 측정 (vDSO bypass 시)
$ sudo perf stat -e 'syscalls:sys_enter_clock_gettime' \
    -e 'syscalls:sys_exit_clock_gettime' \
    -p $PID -- sleep 10

# vDSO 내부 실행 시간 프로파일링
$ sudo perf record -e cpu-clock -g -p $PID -- sleep 5
$ sudo perf report --no-children --sort=symbol | grep -i vdso

# VVAR 페이지 접근 패턴 확인
$ sudo perf mem record -p $PID -- sleep 5
$ sudo perf mem report | grep vvar

# time namespace 전환 시 TLB flush 영향 측정
$ sudo perf stat -e 'dTLB-load-misses,dTLB-store-misses,iTLB-load-misses' \
    -p $PID -- sleep 10

VVAR 페이지 매핑 디버깅

# 프로세스의 VVAR/VDSO 매핑 확인
$ cat /proc/$PID/maps | grep -E '(vvar|vdso)'
7ffce16f5000-7ffce16f9000 r--p 00000000 00:00 0  [vvar]
7ffce16f9000-7ffce16fb000 r-xp 00000000 00:00 0  [vdso]

# 두 프로세스의 VVAR 페이지가 다른 물리 주소인지 확인
# (다른 time NS에 속하면 별도 VVAR 페이지 사용)
$ sudo cat /proc/$PID1/pagemap | xxd | head
$ sudo cat /proc/$PID2/pagemap | xxd | head

# VVAR 페이지 크기 확인 (time NS 활성 시 추가 페이지 할당됨)
$ sudo cat /proc/$PID/smaps | grep -A 15 vvar
Size:                 16 kB    # 일반: 16KB, time NS: 추가 페이지
Rss:                  16 kB
Pss:                   4 kB
AnonHugePages:         0 kB

# systemtap으로 vvar_fault 추적 (아키텍처 디버깅용)
# stap -e 'probe kernel.function("vvar_fault") {
#     printf("%s(%d): vvar fault vmf->pgoff=%d\n",
#            execname(), pid(), $vmf->pgoff)
# }'

디버깅 도구 요약

도구용도관련 명령/옵션
bpftrace오프셋 적용 커널 함수 추적kprobe:timens_add_*
perf statsyscall 오버헤드 측정-e syscalls:sys_*_clock_gettime
perf recordvDSO 프로파일링perf report | grep vdso
stracesyscall 레벨 시간 확인-e clock_gettime,clock_nanosleep
ftrace커널 함수 추적set_ftrace_filter timens_*
nsenterNS 진입 / 시간 비교nsenter -T -t PID
/procNS ID, 오프셋, 매핑 확인ns/time, timens_offsets, maps
lsns시스템 NS 목록lsns -t time

보안 고려사항

보안 이점 호스트 부팅 시간 은닉 컨테이너별 독립 시간축 uptime 기반 서비스 추론 방지 보안 위험 CLOCK_REALTIME으로 호스트 시간 추론 가능 오프셋 오류 시 타이머 기반 보안 무력화 user NS 내에서 임의 생성 가능 권한 요구사항 unshare(CLONE_NEWTIME): CAP_SYS_ADMIN 또는 소유 user NS 내에서 가능 timens_offsets 쓰기: CAP_SYS_TIME (해당 user NS 내에서) nsenter -T: CAP_SYS_ADMIN
보안 주의: Time namespace는 CLOCK_REALTIME을 격리하지 않으므로, 컨테이너 내에서 date 명령이나 CLOCK_REALTIME 기반 시간 조회로 호스트의 실제 시간을 알 수 있습니다. 또한 clock_gettime()/gettimeofday()는 vDSO fast path를 사용할 수 있어 seccomp의 시스템 콜 필터만으로 관측 자체를 완전히 숨길 수 없습니다. wall clock 은닉이 핵심 요구라면 time namespace만으로는 부족하며, 더 강한 격리 계층(VM) 또는 애플리케이션 레벨 시간 주입이 필요합니다.

공격 표면 분석

공격 벡터설명위험도완화 방법
시간 기반 부채널CLOCK_REALTIME으로 호스트 wall clock 추론 후 부팅 시각을 역산중간time namespace만으로 숨길 수 없는 정보임을 전제하고, 필요 시 VM 격리 또는 앱 레벨 시간 주입 사용
타이머 조작네임스페이스 내부의 MONOTONIC/BOOTTIME 오프셋 때문에 컨테이너 내부 만료 판단이 달라질 수 있음높음보안 결정은 신뢰된 네임스페이스의 monotonic clock 또는 외부 검증 서비스에서 수행
로그 위변조컨테이너 내 로그 타임스탬프가 호스트와 불일치낮음중앙 로깅 시스템에서 호스트 시간 사용
임의 NS 생성user NS 내에서 time NS 무제한 생성낮음user.max_time_namespaces sysctl 제한
오프셋 남용극단적 음수 오프셋으로 시간 0 또는 음수 시뮬레이션중간오프셋 범위 검증, 운영 정책

보안 강화 설정

# 1. time namespace 생성 수 제한 (sysctl)
$ sudo sysctl -w user.max_time_namespaces=10
# 시스템 전체에서 최대 10개 time NS만 허용

# 2. seccomp으로 "시간 변경" syscall 제한
# Docker seccomp 프로파일 예시
# {
#   "names": ["clock_settime", "settimeofday", "adjtimex"],
#   "action": "SCMP_ACT_ERRNO",
#   "errnoRet": 1
# }
# 주의: 관측용 clock_gettime()/gettimeofday()는 vDSO 경로 때문에
# seccomp만으로 완전 차단되지 않을 수 있음

# 3. LSM (AppArmor/SELinux) 정책
# AppArmor: time namespace 생성 차단
# deny unshare w,  # CLONE_NEWTIME 포함

# SELinux: time namespace 접근 제어
# allow container_t container_t : nstype { time_ns_create };

# 4. audit 로그로 time NS 생성 추적
$ sudo auditctl -a always,exit -F arch=b64 \
    -S unshare -F a0=0x80 -k timens_create
# 0x80 = CLONE_NEWTIME

# 5. 시간 NS 현황 모니터링
$ sudo lsns -t time
        NS TYPE  NPROCS   PID USER    COMMAND
4026531834 time     100     1 root    /sbin/init
4026532001 time       5  1234 user    /usr/bin/app

보안 모범 사례

보안 권장사항 요약:
  • 모니터링은 호스트 NS에서 실행: MONOTONIC 기반 지표의 오프셋 영향 방지
  • 보안 만료 판정은 신뢰된 MONOTONIC/BOOTTIME 사용: wall clock 점프와 container offset을 동시에 피하기 쉬움
  • 로그 타임스탬프 정책: 컨테이너 로그에 호스트 시간도 함께 기록하거나, 중앙 로깅 서비스 사용
  • namespace 수 제한: user.max_time_namespaces sysctl로 자원 소모 공격 방지
  • 오프셋 검증: CRIU 복원 시 오프셋이 합리적인 범위인지 검증 (예: 10년 이내)
  • CAP_SYS_TIME 제한: 컨테이너에 불필요한 CAP_SYS_TIME capability 부여하지 않기

실전 활용

컨테이너 라이브 마이그레이션

# 1. Source Host에서 checkpoint
$ sudo criu dump -t $CONTAINER_PID -D /tmp/checkpoint \
    --shell-job --tcp-established --ext-unix-sk

# 2. Destination Host로 checkpoint 이미지 전송
$ rsync -avz /tmp/checkpoint/ dest:/tmp/checkpoint/

# 3. Destination Host에서 restore (time namespace 자동 설정)
$ sudo criu restore -D /tmp/checkpoint \
    --shell-job --tcp-established --ext-unix-sk

시간 의존 애플리케이션 테스트

#!/bin/bash
# 미래 시간 시뮬레이션 (uptime +30일)
OFFSET=$((30 * 86400))  # 2,592,000초

sudo unshare --time --fork /bin/bash -c "
  echo 'monotonic ${OFFSET} 0' > /proc/self/timens_offsets
  echo 'boottime ${OFFSET} 0' >> /proc/self/timens_offsets
  exec /bin/bash -c '
    echo \"Simulated uptime: $(cat /proc/uptime)\"
    ./myapp --test-long-running
  '
"

Kubernetes/Docker 통합

# Docker: time namespace 사용 (runc 지원 시)
$ docker run --security-opt=time-namespace=host alpine cat /proc/uptime

# 현재 Docker/containerd는 time NS를 기본 활성화하지 않음
# CRIU를 통한 checkpoint/restore에서 자동 설정

# Kubernetes: CRIU 기반 체크포인트 (실험적)
$ kubectl checkpoint pod/my-pod --container=my-container \
    --export=/tmp/checkpoint

분산 시스템 시간 불일치 시뮬레이션

Time namespace를 활용하여 분산 시스템에서 발생할 수 있는 시간 스큐(skew)를 안전하게 시뮬레이션할 수 있습니다.

#!/bin/bash
# clock-skew-test.sh -- 분산 시스템 clock skew 시뮬레이션
# 3개 노드가 각각 다른 시간 오프셋을 갖는 환경 구성

set -euo pipefail

# 노드별 시간 스큐 (초 단위)
NODE_OFFSETS=(
    0       # 노드 1: 기준 시간
    5       # 노드 2: +5초 앞서감
    -3      # 노드 3: -3초 뒤처짐
)

echo "=== Clock Skew Simulation ==="
echo "Testing Raft consensus with clock offsets: ${NODE_OFFSETS[*]}"

for i in 0 1 2; do
    offset=${NODE_OFFSETS[$i]}
    port=$((8000 + i))

    sudo unshare --time --fork /bin/bash -c "
        if [ '${offset}' != '0' ]; then
            echo 'monotonic ${offset} 0' > /proc/self/timens_offsets
            echo 'boottime ${offset} 0' >> /proc/self/timens_offsets
        fi
        exec ./raft-node --id=${i} --port=${port} --peers=localhost:8000,localhost:8001,localhost:8002
    " &

    echo "Node ${i}: port=${port}, offset=${offset}s, pid=$!"
done

echo "Waiting for consensus test..."
sleep 30

# 결과 수집
echo "=== Test Results ==="
curl -s localhost:8000/status
curl -s localhost:8001/status
curl -s localhost:8002/status

재현 가능한 버그 디버깅

시간 의존 버그를 재현하기 위해 특정 uptime을 시뮬레이션하는 패턴입니다. 예를 들어 "49.7일 후 발생하는 jiffy 래핑 버그"를 테스트할 수 있습니다.

#!/bin/bash
# reproduce-time-bug.sh -- 특정 uptime에서 발생하는 버그 재현

# 32비트 jiffy 래핑 시점 (49.7일 = 4,294,967 초)
TARGET_UPTIME=4294960  # 래핑 7초 전

# 현재 uptime 확인
CURRENT=$(awk '{print int($1)}' /proc/uptime)
OFFSET=$((TARGET_UPTIME - CURRENT))

echo "Current uptime: ${CURRENT}s"
echo "Target uptime:  ${TARGET_UPTIME}s"
echo "Offset needed:  +${OFFSET}s"

sudo unshare --time --fork /bin/bash -c "
    echo 'monotonic ${OFFSET} 0' > /proc/self/timens_offsets
    echo 'boottime ${OFFSET} 0' >> /proc/self/timens_offsets
    exec /bin/bash -c '
        echo \"Simulated uptime: \$(cat /proc/uptime)\"
        echo \"Monitoring for jiffy wrap bug...\"
        ./buggy-driver-test --watch-jiffies --duration=15
    '
"

모니터링 대시보드 통합

#!/bin/bash
# timens-monitor.sh -- 시스템 내 모든 time namespace 모니터링

echo "=== Time Namespace Monitor ==="
printf "%-8s %-20s %-15s %-12s %-12s\n" \
    "PID" "COMMAND" "NS_ID" "MONO_OFFSET" "BOOT_OFFSET"
echo "-------------------------------------------------------------------"

for pid in $(ls /proc/ | grep -E '^[0-9]+$'); do
    [ -r /proc/$pid/ns/time ] || continue

    ns_id=$(readlink /proc/$pid/ns/time 2>/dev/null) || continue
    comm=$(cat /proc/$pid/comm 2>/dev/null) || continue
    offsets=$(cat /proc/$pid/timens_offsets 2>/dev/null) || continue

    # init NS는 건너뜀
    init_ns=$(readlink /proc/1/ns/time 2>/dev/null)
    [ "$ns_id" = "$init_ns" ] && continue

    mono=$(echo "$offsets" | awk '/monotonic/ {printf "%d.%09d", $2, $3}')
    boot=$(echo "$offsets" | awk '/boottime/ {printf "%d.%09d", $2, $3}')

    printf "%-8s %-20s %-15s %-12s %-12s\n" \
        "$pid" "$comm" "$ns_id" "$mono" "$boot"
done

echo "-------------------------------------------------------------------"
echo "Total time namespaces: $(sudo lsns -t time --no-headings | wc -l)"

timens_offsets 구조 심화

timens_offsets 구조체는 Time Namespace의 핵심 데이터입니다. 이 구조체가 커널 내부에서 어떻게 초기화되고, 검증되며, 적용되는지 세부적으로 분석합니다.

오프셋 초기화 경로

/* kernel/time/namespace.c -- clone_time_ns() */
static struct time_namespace *clone_time_ns(
    struct user_namespace *user_ns,
    struct time_namespace *old_ns)
{
    struct time_namespace *ns;

    ns = kmalloc(sizeof(*ns), GFP_KERNEL_ACCOUNT);
    if (!ns)
        return ERR_PTR(-ENOMEM);

    refcount_set(&ns->ns.count, 1);
    ns->vvar_page = NULL;
    ns->user_ns = get_user_ns(user_ns);
    ns->ucounts = inc_time_namespaces(user_ns);
    if (!ns->ucounts) {
        kfree(ns);
        return ERR_PTR(-ENOSPC);
    }

    /* 오프셋을 0으로 초기화 -- 이 시점에서는 frozen=false */
    ns->offsets.monotonic = (struct timespec64){0, 0};
    ns->offsets.boottime  = (struct timespec64){0, 0};
    ns->frozen_offsets = false;

    return ns;
}

오프셋 값 검증 상세

오프셋 쓰기 시 커널은 여러 단계의 검증을 수행합니다.

/* proc_timens_offsets_write() 내부 검증 흐름 */
static ssize_t timens_offsets_write(struct file *file,
    const char __user *buf, size_t count, loff_t *ppos)
{
    struct time_namespace *ns;
    char *kbuf;
    struct proc_timens_offset offsets[2];
    int noffsets;

    /* 검증 1: frozen 상태 확인 */
    ns = current->nsproxy->time_ns_for_children;
    if (ns->frozen_offsets)
        return -EACCES;

    /* 검증 2: user namespace 내 CAP_SYS_TIME 확인 */
    if (!ns_capable(ns->user_ns, CAP_SYS_TIME))
        return -EPERM;

    /* 검증 3: 입력 파싱 ("monotonic sec nsec" 형식) */
    kbuf = memdup_user_nul(buf, count);
    if (IS_ERR(kbuf))
        return PTR_ERR(kbuf);

    noffsets = parse_offsets(kbuf, offsets);

    /* 검증 4: nsec 범위 (0 ~ 999,999,999) */
    for (int i = 0; i < noffsets; i++) {
        if (offsets[i].val.tv_nsec < 0 ||
            offsets[i].val.tv_nsec >= NSEC_PER_SEC)
            return -EINVAL;
    }

    /* 검증 5: clock 이름이 "monotonic" 또는 "boottime"인지 */
    /* 다른 clock 이름은 -EINVAL */

    /* 적용 */
    ns->offsets.monotonic = offsets[0].val;
    ...
}

오프셋 경계 조건

시나리오monotonic 오프셋결과위험도
양수 오프셋+1000초NS 내 시간이 호스트보다 1000초 앞안전
음수 오프셋 (호스트 > |오프셋|)-500초 (호스트 1000s)NS 내 시간 = 500초안전
음수 오프셋 (호스트 < |오프셋|)-2000초 (호스트 1000s)결과 음수, 0으로 클램프위험
매우 큰 양수 오프셋+2^62초overflow 가능위험
나노초 부분만 설정0초 500000000ns0.5초 오프셋안전
초와 나노초 혼합100초 750000000ns100.75초 오프셋안전
경계 주의: 커널은 오프셋 값의 상한을 명시적으로 제한하지 않습니다. 극단적인 값(예: tv_sec = LLONG_MAX)을 설정하면 timespec64_add()에서 overflow가 발생할 수 있습니다. 운영 환경에서는 항상 합리적인 범위(수십 년 이내) 내에서 오프셋을 설정하세요.

/proc/PID/timens_offsets 파일 형식

/* 커널의 timens_offsets 출력 포맷팅 */
static int proc_timens_offsets_show(struct seq_file *m, void *v)
{
    struct time_namespace *ns;
    struct timens_offsets *offsets;

    ns = get_time_ns(current->nsproxy->time_ns_for_children);
    offsets = &ns->offsets;

    /* 형식: "clock_name    seconds    nanoseconds" */
    seq_printf(m, "monotonic %10lld %9lu\n",
               offsets->monotonic.tv_sec,
               offsets->monotonic.tv_nsec);
    seq_printf(m, "boottime  %10lld %9lu\n",
               offsets->boottime.tv_sec,
               offsets->boottime.tv_nsec);

    put_time_ns(ns);
    return 0;
}
핵심 구분: /proc/self/timens_offsets현재 프로세스의 time namespace가 아니라 자식 프로세스가 진입할 time namespace의 오프셋을 보여줍니다. unshare(CLONE_NEWTIME) 전에는 init NS의 오프셋(0, 0)이 표시됩니다.

vDSO Fast Path 심화

Time Namespace에서 vDSO가 시스템 콜 없이 올바른 시간을 반환하는 메커니즘을 아키텍처별로 상세히 분석합니다.

clock_gettime(MONOTONIC) __vdso_clock_gettime() __arch_get_timens_vdso_data() init NS (timens == NULL) 글로벌 vdso_data 사용 custom NS (timens != NULL) NS별 vdso_data 사용 base_time + (cycles * mult >> shift) + offset vdso_read_retry() -- 재시도 timespec 반환

아키텍처별 vDSO 구현

/* lib/vdso/gettimeofday.c -- 아키텍처 공통 vDSO 코드 */
static __always_inline const struct vdso_data *
__arch_get_timens_vdso_data(const struct vdso_data *vd)
{
    /* VVAR 페이지 영역에서 timens용 데이터 위치 계산
     * init NS: NULL 반환 -- 글로벌 vdso_data 사용
     * custom NS: NS별 VVAR 페이지의 vdso_data 포인터 반환
     *
     * x86에서는 VVAR 페이지가 다음과 같이 배치됨:
     *   [VVAR_PAGE]       -- 글로벌 vdso_data (init NS)
     *   [VVAR_TIMENS_PAGE] -- time NS용 vdso_data */
    return (const struct vdso_data *)
        ((char *)vd + VVAR_TIMENS_PAGE_OFFSET * PAGE_SIZE);
}

/* x86 아키텍처: arch/x86/include/asm/vdso/gettimeofday.h */
static __always_inline const struct vdso_data *
__x86_get_timens_vdso_data(const struct vdso_data *vd)
{
    if (likely(!vd->clock_mode))
        return NULL;  /* init NS -- 추가 페이지 불필요 */

    return (const struct vdso_data *)
        ((char *)vd + PAGE_SIZE);
}

VVAR 페이지 매핑 상세

/* arch/x86/entry/vdso/vma.c -- VVAR 페이지 fault 처리 */
static vm_fault_t vvar_fault(
    const struct vm_special_mapping *sm,
    struct vm_area_struct *vma,
    struct vm_fault *vmf)
{
    if (vmf->pgoff == VVAR_DATA_PAGE_OFFSET) {
        /* 글로벌 vdso_data 매핑 */
        return vmf_insert_pfn(vma, vmf->address,
            __pa_symbol(_vdso_data) >> PAGE_SHIFT);
    }

    if (vmf->pgoff == VVAR_TIMENS_PAGE_OFFSET) {
        struct time_namespace *ns = current->nsproxy->time_ns;

        /* init NS이면 fault -- 이 페이지가 필요하지 않음 */
        if (ns == &init_time_ns)
            return VM_FAULT_SIGBUS;

        /* NS별 VVAR 페이지 매핑 */
        return vmf_insert_pfn(vma, vmf->address,
            page_to_pfn(ns->vvar_page));
    }

    return VM_FAULT_SIGBUS;
}
성능 포인트: init time namespace의 프로세스는 VVAR_TIMENS_PAGE에 접근하지 않으므로 추가 오버헤드가 전혀 없습니다. custom time namespace에서만 두 번째 VVAR 페이지가 fault를 통해 lazy하게 매핑됩니다.

CRIU 라이브 마이그레이션 심화

CRIU의 Time Namespace 통합은 단순한 오프셋 설정을 넘어, checkpoint 시점의 시간 정보 수집, 이미지 포맷 내 시간 메타데이터 저장, restore 시 정밀한 오프셋 계산을 포함하는 복잡한 워크플로우입니다.

Phase 1: Checkpoint 1. 프로세스 freeze 2. clock_gettime() 스냅샷 3. timens_offsets 저장 4. 이미지 생성 (.img) Phase 2: Transfer 이미지 전송 (rsync) 시간 메타데이터 포함 Phase 3: Restore 1. unshare(CLONE_NEWTIME) 2. 오프셋 계산 및 쓰기 3. fork() -- 자식 NS 진입 4. 프로세스 복원 + SIGCONT 오프셋 계산 공식 mono_offset = saved_mono - host_B_monotonic boot_offset = saved_boot - host_B_boottime 시간 드리프트 고려 checkpoint ~ restore 사이 경과 시간이 길면 오프셋 정확도 저하. 이미지에 wall clock도 저장하여 보정 활용

CRIU 이미지 내 시간 메타데이터

/* CRIU 복원 코드 (pie/restorer.c 기반 개념 코드) */
static int restore_time_namespace(
    int64_t saved_mono_sec, int64_t saved_mono_nsec,
    int64_t saved_boot_sec, int64_t saved_boot_nsec)
{
    struct timespec host_mono, host_boot;
    int fd;
    char buf[256];

    /* 현재 호스트 시간 측정 */
    clock_gettime(CLOCK_MONOTONIC, &host_mono);
    clock_gettime(CLOCK_BOOTTIME, &host_boot);

    /* 오프셋 계산: saved_time - current_host_time */
    int64_t mono_off_sec = saved_mono_sec - host_mono.tv_sec;
    int64_t mono_off_nsec = saved_mono_nsec - host_mono.tv_nsec;

    /* 나노초 정규화 */
    if (mono_off_nsec < 0) {
        mono_off_sec--;
        mono_off_nsec += 1000000000LL;
    }

    /* timens_offsets 쓰기 */
    fd = open("/proc/self/timens_offsets", O_WRONLY);
    snprintf(buf, sizeof(buf),
        "monotonic %lld %lld\nboottime %lld %lld\n",
        mono_off_sec, mono_off_nsec,
        saved_boot_sec - host_boot.tv_sec,
        saved_boot_nsec - host_boot.tv_nsec);
    write(fd, buf, strlen(buf));
    close(fd);

    return 0;
}

다중 컨테이너 마이그레이션

시나리오오프셋 전략주의사항
단일 컨테이너 이동개별 오프셋 계산단순, 일반적
같은 호스트 내 다중 컨테이너컨테이너별 독립 오프셋각 NS가 독립적
컨테이너 그룹 일괄 이동동일 시점 checkpoint 필요IPC 의존 시 시간 동기화 중요
반복 마이그레이션 (A->B->C)원본 기준 오프셋 재계산정밀도 저하 주의

futex 동작 심화

futex(Fast Userspace muTEX)는 glibc의 pthread_mutex, pthread_cond, sem_wait 등이 내부적으로 사용하는 핵심 동기화 원시 연산입니다. Time Namespace는 futex의 절대 시간 기반 타임아웃에 영향을 미칩니다.

futex clock 선택과 오프셋

/* kernel/futex/futex.h -- futex 타임아웃 처리 */
static int futex_setup_timer(ktime_t *time,
    struct hrtimer_sleeper *timeout,
    int flags, u64 range_ns)
{
    if (!time)
        return 0;

    if (flags & FLAGS_CLOCKRT) {
        /* CLOCK_REALTIME: time NS 무관 */
        hrtimer_init_sleeper_on_stack(timeout,
            CLOCK_REALTIME, HRTIMER_MODE_ABS);
    } else {
        /* CLOCK_MONOTONIC: time NS 오프셋 역변환 필요 */
        hrtimer_init_sleeper_on_stack(timeout,
            CLOCK_MONOTONIC, HRTIMER_MODE_ABS);
    }

    /* 절대 시간을 호스트 시간으로 역변환 */
    timeout->timer.node.expires =
        timens_ktime_to_host(flags & FLAGS_CLOCKRT ?
            CLOCK_REALTIME : CLOCK_MONOTONIC, *time);

    return 0;
}

futex 시나리오별 동작

호출 방식clock시간 유형timens 영향커널 처리
FUTEX_WAITMONOTONIC상대없음직접 사용
FUTEX_WAIT_BITSETMONOTONIC절대오프셋 적용timens_ktime_to_host()
FUTEX_WAIT_BITSET | CLOCK_RTREALTIME절대없음직접 사용
FUTEX_LOCK_PIREALTIME절대없음직접 사용

pthread 라이브러리 영향

/* pthread_cond_timedwait은 내부적으로 futex 사용
 *
 * 기본 clock: CLOCK_REALTIME -- timens 영향 없음
 * CLOCK_MONOTONIC으로 설정 시: timens 오프셋 적용
 *
 * 시나리오: time NS (offset +5000s) 내에서 */
struct timespec abs;
clock_gettime(CLOCK_MONOTONIC, &abs);  /* NS 시간: 6000s */
abs.tv_sec += 10;                      /* 6010s에 타임아웃 */

pthread_cond_timedwait(&cond, &mutex, &abs);
/* 커널: timens_ktime_to_host(MONOTONIC, 6010s)
 *       6010 - 5000 = 1010s (호스트 시간)
 * hrtimer: 호스트 1010s에 만료
 * 호스트 현재 1000s -- 10초 후 만료 (정확) */

timerfd 동작 심화

timerfd는 파일 디스크립터 기반 타이머로, epoll/select/poll 이벤트 루프에서 핵심적으로 사용됩니다. Time Namespace에서 timerfd의 정확한 동작을 이해하는 것은 컨테이너 내 이벤트 루프 기반 애플리케이션의 안정성에 직결됩니다.

timerfd 커널 내부 경로

/* fs/timerfd.c -- timerfd_settime 내부 */
static int do_timerfd_settime(int ufd, int flags,
    const struct itimerspec64 *new,
    struct itimerspec64 *old)
{
    struct timerfd_ctx *ctx = file->private_data;
    ktime_t expires;

    if (flags & TFD_TIMER_ABSTIME) {
        /* 절대 시간: namespace 시간 -- 호스트 시간 변환 */
        expires = timespec64_to_ktime(new->it_value);
        expires = timens_ktime_to_host(ctx->clockid, expires);
    } else {
        /* 상대 시간: 변환 불필요 */
        expires = timespec64_to_ktime(new->it_value);
    }

    hrtimer_start(&ctx->t.tmr, expires,
        (flags & TFD_TIMER_ABSTIME) ?
        HRTIMER_MODE_ABS : HRTIMER_MODE_REL);
    return 0;
}

timerfd clock 유형별 영향

timerfd clock절대 시간 (ABSTIME)상대 시간timens 영향
CLOCK_MONOTONIC오프셋 역변환영향 없음mono 오프셋
CLOCK_BOOTTIME오프셋 역변환영향 없음boot 오프셋
CLOCK_REALTIME영향 없음영향 없음없음
CLOCK_BOOTTIME_ALARM오프셋 역변환영향 없음boot 오프셋
이벤트 루프 호환성: libuv, libevent, libev 등의 이벤트 루프 라이브러리는 일반적으로 timerfd를 상대 시간으로 사용하므로 time namespace에서 별도 수정 없이 정상 동작합니다.

timer_list과 hrtimer 내부

커널 내부의 타이머 서브시스템이 time namespace 오프셋을 어떻게 처리하는지 분석합니다.

hrtimer 오프셋 변환 함수

/* kernel/time/namespace.c -- 핵심 변환 함수 */
ktime_t timens_ktime_to_host(clockid_t clockid, ktime_t tim)
{
    struct time_namespace *ns = current->nsproxy->time_ns;
    ktime_t offset;

    if (ns == &init_time_ns)
        return tim;  /* init NS: 변환 불필요 */

    switch (clockid) {
    case CLOCK_MONOTONIC:
    case CLOCK_MONOTONIC_RAW:
    case CLOCK_MONOTONIC_COARSE:
        offset = timespec64_to_ktime(ns->offsets.monotonic);
        break;
    case CLOCK_BOOTTIME:
    case CLOCK_BOOTTIME_ALARM:
        offset = timespec64_to_ktime(ns->offsets.boottime);
        break;
    default:
        return tim;
    }

    /* host_time = ns_time - offset */
    return ktime_sub(tim, offset);
}

커널 모듈 타이머 호환성

커널 타이머 API시간 기준timens 영향
hrtimer_start(ABS)호스트 절대 시간커널 내부, 영향 없음
hrtimer_start(REL)상대 시간영향 없음
mod_timer()jiffies 기반영향 없음
schedule_timeout()jiffies 기반영향 없음
userspace timer_settime(ABS)NS 절대 시간오프셋 역변환
핵심 원칙: 커널 내부 타이머(hrtimer, timer_list)는 항상 호스트 시간 기준으로 동작합니다. Time namespace 오프셋 변환은 커널-유저 경계(시스템 콜 진입/반환)에서만 발생합니다. 커널 모듈 개발자는 timens를 특별히 고려할 필요가 없습니다.

다른 네임스페이스와의 상호작용

Time Namespace는 독립적으로 동작하지만, 다른 namespace와 결합될 때 중요한 상호작용이 존재합니다.

Time Namespace timens_offsets, vvar_page PID Namespace /proc/PID/timens_offsets Mount Namespace /proc 마운트 연동 User Namespace CAP_SYS_TIME 권한 검사 Network Namespace TCP 타임스탬프 영향 없음

PID Namespace와의 상호작용

/proc/PID/timens_offsets 접근 시 PID namespace가 PID 번역을 담당합니다. 중첩 PID NS에서 외부 PID(예: 12345)와 내부 PID(예: 1)가 같은 프로세스를 가리킵니다.

Mount Namespace와 /proc

# 일반적인 namespace 조합 (mount NS 포함)
$ unshare --pid --time --mount --fork /bin/bash

# 새 /proc 마운트 (mount NS 내에서)
$ mount -t proc proc /proc

# 이제 /proc/uptime은 time NS 오프셋 반영
# mount NS 없이 time NS만 사용하면 /proc 정보 불일치 가능

User Namespace와 권한

작업필요 권한User NS 의미
unshare(CLONE_NEWTIME)CAP_SYS_ADMIN현재/상위 user NS에서 보유
timens_offsets 쓰기CAP_SYS_TIME소유 user NS에서 보유
비특권 time NS없음user NS 내에서 자동 허용
# 비특권 사용자도 user NS 내에서 time NS 생성 가능
$ unshare --user --time --map-root-user --fork /bin/bash

# user NS 내에서 root로 매핑됨
$ id
uid=0(root) gid=0(root)

# timens_offsets 쓰기 가능
$ echo "monotonic 1000 0" > /proc/self/timens_offsets
네트워크 타임스탬프: TCP 타임스탬프(tcp_timestamps)는 jiffies 기반이므로 time namespace 영향을 받지 않습니다. SO_TIMESTAMP 패킷 타임스탬프도 호스트 기준입니다.

보안 고려사항 심화

CAP_SYS_TIME과 Time Namespace

/* CAP_SYS_TIME의 범위 */

/* 1. settimeofday() -- 전역 CLOCK_REALTIME 변경
 *    CAP_SYS_TIME (init user NS에서만 유효)
 *    time NS와 무관 */

/* 2. timens_offsets 쓰기
 *    CAP_SYS_TIME (해당 time NS의 소유 user NS에서)
 *    user NS 내에서도 가능 */

/* 3. adjtimex() -- NTP 조정
 *    CAP_SYS_TIME (init user NS)
 *    time NS와 무관 */

보안 강화 권장사항

# 1. seccomp로 time NS 생성 차단
# CLONE_NEWTIME = 0x80
# Docker seccomp 프로필에서 unshare 제한 가능

# 2. sysctl로 비특권 NS 생성 제한
$ sysctl kernel.unprivileged_userns_clone=0

# 3. time NS 최대 개수 제한
$ cat /proc/sys/user/max_time_namespaces
31924
$ echo 100 > /proc/sys/user/max_time_namespaces

# 4. audit 로그로 time NS 생성 추적
$ sudo auditctl -a always,exit -F arch=b64 \
    -S unshare -F a0=0x80 -k timens_create
공격 시나리오: 악의적인 컨테이너가 극단적인 오프셋(+100년)을 설정하면 CLOCK_MONOTONIC 기반 만료 메커니즘(토큰, 캐시)이 우회될 수 있습니다. 보안 결정이 호스트 밖에서 공유되어야 한다면, 컨테이너 내부 clock 대신 신뢰된 외부 시간원을 기준으로 판정해야 합니다.

테스트 및 디버깅 심화

커널 셀프테스트

# 커널 소스의 Time Namespace 셀프테스트
$ cd /usr/src/linux
$ make -C tools/testing/selftests/timens run_tests

# 개별 테스트
$ cd tools/testing/selftests/timens
$ ./timens          # 기본 오프셋
$ ./timerfd         # timerfd 오프셋
$ ./timer           # POSIX 타이머
$ ./clock_nanosleep # nanosleep
$ ./procfs          # /proc 인터페이스
$ ./exec_after_fork # fork+exec 후 오프셋 유지

고급 추적 기법

# bpftrace로 timens 오프셋 적용 모니터링
$ sudo bpftrace -e '
kprobe:timens_ktime_to_host {
    printf("pid=%d comm=%s clock=%d\n", pid, comm, arg0);
}
kretprobe:timens_ktime_to_host {
    printf("  result=%lld\n", retval);
}'

# /proc/PID/maps에서 VVAR 매핑 확인
$ grep vvar /proc/$$/maps
7ffce1a00000-7ffce1a02000 r--p 00000000 00:00 0  [vvar]
# 2페이지 = init NS + timens 페이지

# perf로 vDSO fallback 감지
$ sudo perf record -e 'syscalls:sys_enter_clock_gettime' -p $PID
# vDSO 정상이면 이벤트 거의 없음. 많으면 fallback 발생

자주 발생하는 문제

문제원인해결
CLONE_NEWTIME not supported커널 5.6 미만 또는 CONFIG_TIME_NS=n커널 업그레이드
오프셋 후 자식에서 변화 없음time_ns_for_children에 미설정unshare 후 확인
Java System.nanoTime() 불일치JVM이 CLOCK_MONOTONIC 사용정상 (오프셋 적용됨)
Go time.Now() 이상monotonic clock 병행 사용정상 동작

다른 시간 가상화 접근법 비교

Linux Time Namespace 외에도 시간 가상화를 달성하는 여러 방법이 있습니다.

접근법 비교 표

접근법격리 수준대상 clock성능 영향커널 요구주요 사용처
Time Namespace커널 수준MONOTONIC, BOOTTIME거의 없음Linux 5.6+컨테이너, CRIU
libfaketime라이브러리REALTIME, MONOTONICLD_PRELOAD없음테스트, 개발
KVM/QEMU하이퍼바이저모든 clock가상화 비용KVMVM 격리
seccomp+ptrace시스템 콜선택적ptrace 비용없음샌드박스
gVisor인터셉트모든 clock시스템 콜 비용없음보안 컨테이너

libfaketime과의 비교

# libfaketime: LD_PRELOAD로 시간 함수 후킹
$ LD_PRELOAD=/usr/lib/libfaketime.so.1 FAKETIME="+1y" date
# 출력: 내년 날짜 (CLOCK_REALTIME도 변경)

# libfaketime 한계 (vs Time Namespace):
# 1. vDSO 경로 우회 불가 -- clock_gettime() 미후킹 가능
# 2. 멀티프로세스 일관성 보장 어려움
# 3. setuid 바이너리에서 LD_PRELOAD 무시
# 4. 커널 내부 타이머에 영향 불가
# 5. Go/Rust non-glibc 런타임에서 불완전
적합한 선택 기준:
  • 컨테이너 CRIU 마이그레이션: Time Namespace (유일한 올바른 선택)
  • 테스트에서 REALTIME 변경: libfaketime
  • wall clock까지 포함한 강한 시간 격리: KVM/QEMU
  • Go/Rust 바이너리: Time Namespace (libfaketime 불완전)

참고자료

다음 학습: