사전 CLOCK_TAI
개념

CLOCK_TAI

gabury1고친 사람 github-actions[bot]

CLOCK_TAI 는 윤초가 들어도 끊기지 않는 시각을 리눅스에서 읽어 오는 시계입니다. 보통의 시계는 윤초 때 같은 1초를 두 번 셉니다. 이 시계는 그 1초도 되풀이하지 않고 이어 셉니다. 대신 윤초가 몇 번 들었는지 커널이 알고 있어야 값이 맞습니다.

쉽고 빠른 이해

무슨 일을 하는 물건인가 — 리눅스가 들고 있는 여러 시계 가운데 윤초에도 끊기지 않고 1초씩 이어 세는 시계를 고르는 이름입니다. 제대로 설정된 기계에서 이 시계는 보통의 시계보다 37초 앞서 있습니다.

왜 이렇게 하나 — 달력 시각은 지구의 자전에 맞추려고 가끔 1초를 끼워 넣습니다. 그 1초 동안 보통의 시계는 같은 초를 두 번 셉니다. 그래서 그 1초를 사이에 둔 두 시각을 빼면 흐른 시간보다 1초 적게 나옵니다.

어떻게 도나

  1. 시각 맞추기 프로그램이 달력 시각과 원자시계 시각의 차이를 커널에 알려 줍니다
  2. 커널은 보통의 시계에 그 차이를 더해 이 시계의 값을 냅니다
  3. 1초를 끼워 넣는 순간 보통의 시계는 1초 되돌아갑니다. 같은 순간 차이는 1 늡니다. 되돌아간 1초를 늘어난 1이 메워 이 시계는 끊기지 않습니다

대가 — 아무도 차이를 알려 주지 않으면 이 시계는 보통의 시계와 똑같은 값을 냅니다. 오류도 나지 않습니다. 관리자가 보통의 시계를 고치면 이 시계도 함께 뜁니다. 다른 운영체제에는 이 이름이 없을 수 있습니다.

상세

쪽 번호가 가끔 되풀이되는 책을 떠올려 보세요. 36쪽을 넘겼습니다. 그런데 다음 장에도 36이 한 번 더 적혀 있는 식입니다. 이런 책에서 쪽 번호끼리 빼면 넘긴 장 수보다 적게 나옵니다. 인쇄소는 이 문제를 막으려고 종이 귀퉁이에 번호를 하나 더 찍습니다. 쪽 번호에 앞에서 번호가 되풀이된 횟수를 더한 번호입니다.

CLOCK_TAI 는 운영체제 안의 그 귀퉁이 번호입니다. 되풀이되는 쪽 번호는 사람이 읽는 시각을 내는 리눅스의 보통 시계, CLOCK_REALTIME 입니다.

운영체제의 시계 가운데 하나를 고르는 이름

응용 프로그램은 시각을 스스로 세지 않습니다. 운영체제의 중심부인 커널에 물어봅니다. 커널은 쓰임이 다른 시계를 여러 개 들고 있습니다. 어느 시계를 읽을지는 이름을 넘겨 고릅니다.

시계를 읽는 호출은 clock_gettime 입니다. 첫 인자로 시계 이름을 넘기면 초와 나노초가 돌아옵니다. 나노초는 10억분의 1초입니다.

시계 이름은 대부분 POSIX(Portable Operating System Interface, 이식 가능한 운영체제 인터페이스)가 정해 둡니다. POSIX 는 리눅스와 macOS 같은 유닉스 계열 운영체제가 함께 지키는 규격입니다. CLOCK_TAI 는 그 목록에 없습니다. 리눅스가 따로 더한 이름이어서 다른 운영체제에는 없거나 뜻이 다를 수 있습니다.

C 프로그램에서는 이 이름을 헤더 time.h 의 상수로 씁니다. 헤더는 함수와 상수의 선언을 모아 둔 파일입니다. 파이썬에서는 time.CLOCK_TAI 로 부릅니다.

이름 속 TAI 와 윤초

이름의 TAI 는 TAI(Temps Atomique International, 국제원자시)를 가리킵니다. TAI 는 세계 곳곳의 원자시계가 센 초를 모아 끊지 않고 이어 세는 기준 시각입니다. 1초를 끼우거나 빼는 일이 없습니다.

달력과 벽시계가 쓰는 시각은 UTC(Coordinated Universal Time, 협정 세계시)입니다. UTC 도 원자시계의 초를 씁니다. 그런데 지구의 자전과 너무 벌어지지 않도록 가끔 1초를 끼워 넣습니다. 이 1초를 윤초라고 부릅니다.

윤초가 들 때마다 UTC 는 TAI 보다 1초씩 더 뒤처집니다. 마지막 윤초는 2016년 마지막 날 끝에 들었습니다. 그 뒤로 UTC 는 TAI 보다 37초 뒤에 있습니다.

CLOCK_REALTIME 이 윤초에 1초를 되풀이하는 까닭

CLOCK_REALTIME 은 1970년 1월 1일 0시 UTC 부터 흐른 초를 셉니다. 이 셈법을 유닉스 시간이라고 부릅니다. 유닉스 시간은 하루를 늘 86400초로 칩니다. 그래서 윤초 1초에 붙일 번호가 없습니다.

리눅스는 윤초가 드는 1초 동안 CLOCK_REALTIME 을 1초 되돌려 같은 초를 한 번 더 셉니다. 그 1초 사이에 찍은 로그는 바로 앞 1초에 찍은 로그와 같은 시각을 받습니다. 윤초를 사이에 둔 두 시각을 빼면 흐른 시간보다 1초 적게 나옵니다.

CLOCK_REALTIME 에 차이를 더한 값

CLOCK_TAI 의 값은 CLOCK_REALTIME 에 TAI 와 UTC 의 차이를 더한 것입니다. 커널은 이 차이를 초 단위 값 하나로 들고 있습니다.

윤초가 들어 CLOCK_REALTIME 이 1초 되돌아가는 순간, 커널은 이 차이를 1 늘립니다. 되돌아간 1초를 늘어난 1이 메웁니다. 그래서 CLOCK_TAI 는 끊기지 않고 1초씩 갑니다.

아래 표는 2016년 마지막 윤초 앞뒤 3초 동안 두 시계가 가리킨 값입니다. 가운데 줄이 끼워 넣은 윤초입니다.

UTC 시각 CLOCK_REALTIME 더하는 차이 CLOCK_TAI
12월 31일 23:59:59 1483228799 36 1483228835
12월 31일 23:59:60 1483228799 37 1483228836
1월 1일 00:00:00 1483228800 37 1483228837

CLOCK_REALTIME 열에는 1483228799 가 두 번 나옵니다. CLOCK_TAI 열은 끝자리가 835 · 836 · 837 로 한 칸씩 올라갑니다.

차이가 제대로 들어간 기계에서 두 시계를 잇달아 읽으면 이렇게 나옵니다. 아래는 파이썬으로 읽은 예입니다.

Python
import time
r = time.CLOCK_REALTIME
t = time.CLOCK_TAI
a = time.clock_gettime(r)  # 1758772800.412
b = time.clock_gettime(t)  # 1758772837.412
round(b - a)               # 37

a 는 유닉스 시간입니다. b 는 거기에 37을 더한 값입니다. 두 줄을 읽는 사이의 짧은 틈은 반올림으로 지웠습니다.

차이를 커널에 넣는 데몬

커널은 윤초가 몇 번 들었는지 스스로 알 방법이 없습니다. 윤초는 계산으로 미리 정해지지 않습니다. 지구의 자전을 지켜보고 그때그때 정합니다. 그래서 누군가 이 차이를 커널에 넣어 줘야 합니다.

이 일은 시각을 맞추는 데몬이 맡습니다. 데몬은 뒤에서 늘 떠 있는 프로그램입니다. 시각 맞추기 데몬은 바깥의 시각 서버에 물어 기계의 시계를 맞춥니다.

바깥 시각 서버와 시계를 맞추는 대표적인 방식이 둘 있습니다. NTP(Network Time Protocol, 네트워크 시간 프로토콜)는 인터넷 너머의 서버와 맞출 때 흔히 쓰는 방식입니다. PTP(Precision Time Protocol, 정밀 시간 프로토콜)는 한 네트워크 안의 기기끼리 더 촘촘히 맞추는 방식입니다.

데몬이 차이를 알려면 윤초 표가 있어야 합니다. 윤초 표는 지금까지 든 윤초와 그때마다의 TAI 와 UTC 의 차이를 적은 목록입니다. 데몬은 이 표에서 읽은 차이를 adjtimex 호출로 커널에 넣습니다.

flowchart TD
    A["윤초 표"] --> B["시각 맞추기 데몬"]
    B -->|"adjtimex 로 넣는다"| C["커널이 들고 있는 차이 · 처음엔 0"]
    D["CLOCK_REALTIME"] --> E["CLOCK_TAI · CLOCK_REALTIME + 차이"]
    C --> E

아무도 차이를 넣지 않은 기계에서는 이 값이 0 으로 남습니다. 그러면 CLOCK_TAI 가 CLOCK_REALTIME 과 똑같은 값을 냅니다. 오류가 나지 않아서 틀린 줄 모르고 지나가기 쉽습니다. 두 시계를 한 번씩 읽어 차이가 0 인지 보면 설정이 됐는지 알 수 있습니다.

CLOCK_REALTIME 을 따라 뛰는 값

CLOCK_TAI 에 값을 직접 쓸 수는 없습니다. 시계 값을 바꾸는 호출 clock_settime 에 이 이름을 넘기면 EINVAL(invalid argument, 잘못된 인자) 오류가 돌아옵니다.

그렇다고 CLOCK_TAI 가 늘 앞으로만 가는 것은 아닙니다. 값이 CLOCK_REALTIME 에서 나오기 때문입니다. 관리자가 CLOCK_REALTIME 을 한 시간 되돌리면 CLOCK_TAI 도 한 시간 되돌아갑니다.

CLOCK_TAI 가 막아 주는 끊김은 윤초 때의 1초뿐입니다. 한 기계 안에서 걸린 시간을 잴 때는 누구도 되돌릴 수 없는 CLOCK_MONOTONIC 을 씁니다.

이 시계로 기다리기

프로그램은 시계를 읽기만 하지 않습니다. 시계가 어떤 값에 이를 때까지 기다리기도 합니다. 기다리는 호출 가운데 CLOCK_TAI 를 받는 것과 안 받는 것이 갈립니다.

호출 하는 일 CLOCK_TAI
clock_nanosleep 정한 시간 동안 또는 정한 시각까지 잠든다 받는다
timer_create 정한 시각에 알림을 보내는 타이머를 만든다 받는다
timerfd_create 타이머가 울리기를 파일 읽기처럼 기다린다 안 받는다

정한 시각까지 잠들 때 CLOCK_TAI 를 고르면 그 시각을 TAI 로 셉니다. 기다리는 사이에 윤초가 끼어도 1초 늦게 깨지 않습니다.

세 시계를 고르는 기준

리눅스에서 백엔드가 흔히 만나는 시계는 셋입니다. 무엇을 하려는지에 따라 고르는 시계가 다릅니다.

하려는 일 고르는 시계
로그와 데이터베이스에 사람이 읽는 시각을 적는다 CLOCK_REALTIME
한 기계 안에서 걸린 시간을 잰다 CLOCK_MONOTONIC
여러 기계의 시각을 견주며 윤초 너머까지 흐른 초를 센다 CLOCK_TAI

CLOCK_TAI 값을 로그에 그대로 찍으면 사람이 아는 시각보다 37초 앞선 값이 적힙니다. 그래서 사람에게 보여 줄 시각은 UTC 로 바꿔서 적습니다.

CLOCK_TAI 는 1초의 되풀이도 견디지 못하는 일에서 고릅니다. PTP 로 기기끼리 시각을 아주 촘촘히 맞추는 시스템이 그런 예입니다. PTP 는 시각을 TAI 로 나르므로 받은 값을 이 시계와 바로 견줄 수 있습니다.

관련 항목

CLOCK_TAI 와 나란히 선 리눅스 시계

CLOCK_REALTIME · CLOCK_MONOTONIC · CLOCK_MONOTONIC_RAW · CLOCK_BOOTTIME · CLOCK_REALTIME_COARSE · CLOCK_PROCESS_CPUTIME_ID

CLOCK_TAI 를 읽고 기준으로 삼는 호출

clock_gettime · clock_getres · clock_settime · clock_nanosleep · timer_create · timerfd_create · vDSO

CLOCK_TAI 에 더할 차이를 커널에 넣는 호출과 데몬

adjtimex · clock_adjtime · ntp_adjtime · 데몬 · chrony · ntpd · linuxptp

CLOCK_TAI 가 따르는 시간 척도

TAI · UTC · GPS 시간 · 유닉스 시간 · 원자시계

CLOCK_TAI 가 넘어서는 윤초 보정

윤초 · 윤초 표 · leap-seconds.list · 윤초 문지르기 · DTAI

CLOCK_TAI 를 기기 사이에 맞추는 프로토콜

PTP · NTP · 시간 동기화 · GPS

CLOCK_TAI 값을 담는 타입과 단위

struct timespec · clockid_t · time.h · 나노초 · time.CLOCK_TAI

CLOCK_TAI 가 속하는 상위 개념

시간과 시계 · 벽시계 · 단조 시계 · 시스템 시계 · 타임스탬프

CLOCK_TAI 를 정하고 내주는 소프트웨어와 규격

리눅스 · 커널 · 운영체제 · POSIX · 유닉스

다른 이름: clock_tai · TAI 시계