Представим ситуацию: Вы нашли в кладовой старый компьютер и решили попробовать его включить. Компьютер запускается, хотя стоял без дела 10 лет, но есть проблема: система уверена, что сегодня 1 января 1970 года. Эта ситуация поможет разобраться, как компьютеры хранят текущее время: ведь у них нет внутри пружин или маятников, как в часах, которые могли бы отсчитывать секунды. Каким образом они показывают верное время, если отключить их на несколько часов, и почему тот же принцип не работает, если компьютер простоял в кладовке 10 лет?
Прежде, чем ответить на эти вопросы, разберемся, как измеряется точное время. Это не такая простая задача, если под рукой нет часов. Мы можем отсчитывать дни, месяцы и годы или приблизительно разбить день на часы. Но найти ориентир, который подскажет количество прошедших секунд, гораздо сложнее.
Принято считать, что первым эту задачу в XVII веке решил итальянский ученый Галилео Галилей. Галилео нужно было точно знать, сколько длился тот или иной эксперимент. Его биографы утверждают, что сначала он считал количество своих сердцебиений, но однажды наблюдал за раскачивающимся канделябром в кафедральном соборе Пизы и заметил, что каждое колебание канделябра занимает одинаковое количество сердцебиений. Это число не меняется при затухании амплитуды движений. Так он понял, что колебания маятника помогают точно измерять время.
Когда в 70-е-80-е годы инженеры впервые столкнулись с задачей отображения человеческого времени в компьютере, они, как ни странно, обратились к трудам итальянского ученого, но взглянули на них под другим углом. Маятник — не единственный возможный источник колебаний. У любого современного персонального компьютера есть «сердце» — центральное процессорное устройство (ЦПУ), или просто процессор. Он устроен очень сложно и выполняет множество функций, но в данном случае интересно одно из составляющих его устройств. Когда через него проходит ток, оно испускает равномерные электрические колебания с одинаковой частотой. Частота этих колебаний измеряется в герцах (Гц) и определяет количество операций, которое процессор может выполнить за одну секунду. К сожалению, если мы будем использовать единственную доступную нам операцию в секунду для увеличения счетчика колебаний, то наш процессор будет совершенно непригоден для чего-то ещё: ему будет некогда исполнять другие команды. Конечно, современные процессоры не такие медленные, их частота измеряется в гигагерцах (ГГц), то есть в 1 000 000 000 операций в секунду. Но даже при такой мощности для измерения времени пришлось бы считать каждое колебание процессора. Для решения проблемы был придуман кварцевый генератор. Он представляет собой тончайшую кремниевую пластину, которая под воздействием электрического тока равномерно расширяется и сжимается, генерируя слабый электрический заряд на поверхности. Таким образом, механические колебания пластины сопровождаются синхронными, равномерными колебаниями электрического заряда.
Полученные равномерные колебания принято называть таймером — они позволяют процессору синхронизировать производимые им операции во времени. Таким образом, у компьютера есть своё собственное понимание времени.
Получается, что внутри компьютера есть таймер, но он не измеряет человеческое время — оно для него довольно бесполезно. Как правило, эту задачу на себя берут операционные системы (ОС). Поскольку ОС знает, с какой частотой работает кварцевый генератор, она может измерить время, которое проходит между срабатываниями таймера (срабатывание таймера называют тик (tick) или джиффи (jiffy)). Если генератор работает на частоте 100 Гц, то период между тиками равен 1/100 секунды или 10 миллисекундам. Операционная система создает в памяти переменную, которую обычно называют jiffies, и увеличивает её на единицу каждый раз, когда процессор дает сигнал о новом тике.
Соответственно, чтобы узнать, как долго включен компьютер, системе достаточно умножить размер периода между тиками на количество этих самых тиков. А чтобы узнать текущее время, нужно просто добавить прошедшее время к времени на момент старта системы. Но как узнать человеческое время на момент старта системы?
Первые персональные компьютеры (например, IBM PC или Apple II), не умели следить за тем, сколько прошло время после выключения, а спрашивали его на старте. Для решения этой задачи снова пригодился кварцевый генератор. Устройству неважно, из какого источника получать электрический ток, — это натолкнуло разработчиков на мысль, что достаточно подключить генератор к обычной литиевой батарейке, чтобы получать равномерные электрические колебания.
Если в эту схему добавить бинарный счетчик, который увеличивается на каждое колебание, то мы получим устройство, которое может фиксировать человеческое время. Оно так и называется — RTC (Real Time Clock), или часы реального времени. Частота колебаний на выходе из RTC обычно 32 768 Гц или 215 Гц, что удобно использовать в бинарных счетчиках.
Старый компьютер, о котором шла речь в начале, перепутал время именно из-за того, что батарейка в его часах реального времени села: после запуска он не смог считать время из RTC и выставил стандартное стартовое время — 1 января 1970 года.
RTC позволяет компьютеру отсчитывать миллисекунды, даже когда он выключен. Но стандартные RTC имеют погрешность 1,7-8,6 секунд в день — то есть за год они могут потерять целый час.
Иногда мы сталкиваемся с этой проблемой в реальной жизни: например, когда нам приходится вручную настраивать время на наручных кварцевых часах или на микроволновке, где тоже используют RTC. Но нам никогда не приходится делать этого на компьютере. Да и на старте компьютер больше не спрашивает текущее время. Современные компьютеры настраивают время через интернет — для этого используется NTP (протокол сетевого времени). Этот протокол даже учитывает время передачи данных между источником и компьютером и компенсирует его. В публичной сети погрешность составляет всего 10 мс.
Когда наш компьютер выключен, RTC продолжает работу и отсчитывает время. Когда мы нажимаем кнопку запуска, операционная система забирает время из RTC и начинает отслеживать время самостоятельно, используя таймер процессора. Время от времени операционная система получает точное время по NTP и поправляет свой внутренний счетчик. Когда мы выключаем компьютер, за дело снова берется RTC.
Как уже говорилось, для отслеживания человеческого времени операционная система создает в памяти компьютера переменную jiffy, в которой хранит количество тиков с момента старта системы. Но как с помощью неё показывать человеку календарь?
В 70-е годы прошлого века эту проблему решили инженеры из Bell Labs при разработке операционной системы Unix (она заложила фундамент для появления современных Linux и MacOS). Они ввели в систему переменную, которая, начиная с заданной даты, увеличивается на каждый тик генератора — её называли epoch.
Под эту переменную отводилось целое число со знаком (signed integer) размером в 32 бита (то есть от −2 147 483 648 (-231) до 2 147 483 647 (231−1)). Подавляющее большинство генераторов на тот момент работали на частоте 60 Гц, то есть отсчитывали 60 тиков в секунду, поэтому в переменной хранилось 1/60 секунды и она могла представлять временной промежуток не более 829 дней.
В версии Unix от 1971 года отсчет начинался с 1971-01-01 00:00:00. На следующий год с 1972-01-01 00:00:00. Переводить время каждый год было довольно неудобно, поэтому в четвертой версии Unix в 1973 году за epoch была взята дата 1970-01-01 00:00:00, а в переменной стали хранить не 1/60 секунды, а полную секунду. Позже этот принцип стал международным стандартом и используется по сей день.
Если на Вашем компьютере установлена операционная система семейства Linux или MacOS, Вы можете увидеть текущий Unix-timestamp, введя в терминале:
date +"%s"
Windows тоже считает время, отталкиваясь от конкретной даты, но использует для этого не абстрактный 1970 год, а 1601 — первый год Григорианского календаря.
Поскольку время хранится в целочисленной 32-битовой переменной и представляет собой определенное количество секунд с определенного момента, то самое большое количество секунд, которое мы можем использовать — это 2 147 483 647 (231−1). Если прибавить это количество к epoch, то мы получим 19 января 2038 03:14. Что произойдет с системой, когда наступит этот день и пройдет еще одна секунда?
Чтобы ответить на этот вопрос, рассмотрим особенность 32-битовых переменных:
2 147 483 647 в двоичной системе записывается как
01111111111111111111111111111111 // (1 ноль и 31 единица)
Если это число увеличить на единицу, то оно превратится в
10000000000000000000000000000000 // (1 единица и 31 ноль)
Первая цифра у таких чисел означает знак: +
или -
. А значит, что в десятичной системе оно будет равно −2 147 483 648, то есть число перейдет от самого большого положительного к самому маленькому отрицательному. И система начнет показывать дату, равную разности epoch и двух млрд секунд, то есть 13 декабря 1901 года. Но не стоит волноваться, проблема уже решена — большинство систем используют 64 битовые числа для хранения времени. Этого хватит, чтобы не столкнуться с проблемой до 15:40 4 декабря 292 277 026 596 года.
«640 килобайт памяти должно хватить кому угодно», — Билл Гейтс.
На самом деле в истории уже была такая ситуация. В 60-70-е годы прошлого столетия, когда люди только начали писать программное обеспечение для компьютеров, платы памяти стоили дорого и большинство компьютеров обходились несколькими килобайтами. Ради экономии памяти программисты решили записывать даты в формате ДД.ММ.ГГ.
Какую проблему это порождало? Допустим, у нас есть человек по имени Боб, у которого дата рождения записана как 01.11.19. Он родился в 1919 году, и ему чуть больше 100 лет. Есть человек по имени Фред и он родился 02.11.19, но ему всего два года, потому что он родился в 2019 году. Сталкиваясь с таким форматом дат, человек может исходить из контекста, но компьютер на такое не способен.
Программисты, которые писали программы в 60-70-е годы даже не предполагали, что их код может дожить до 2000 года, поэтому использование двух цифр вместо четырех было нормальной оптимизацией. Но когда приближался 2000 год, а многие компании всё ещё использовали тот же формат дат, началась паника: никто не понимал, что произойдёт, когда 99 год сменится на 00. Тогда в мире ходили самого разного рода слухи: например, что банкоматы в этот момент начнут плеваться деньгами, а самолёты начнут падать. Баг был вовремя замечен и проблему в большинстве систем удалось исправить вовремя.
Представим, что абстрактный программист пишет приложение, которое должно каждый день в 12 часов дня отправлять пользователю уведомление, что наступил полдень. Программист живет в Лондоне и оно будет использоваться только там. Писать код он начал зимой, поэтому разницы с UTC(Всемирным координированным временем) нет.
Приложение работало всю зиму, но в первый день апреля уведомление ушло на час позже полудня. Дело в том, что разработчик просто прикрутил таймер, который срабатывает раз в 24 часа, а в последний день марта в Великобритании перевели время на час вперед — на летнее. Разработчик быстро исправил ситуацию, заменив таймер на планировщик задач — последний срабатывает в соответствии с локальным временем сервера, который находится в квартире разработчика.
Как ни странно, приложение пользуется популярностью. Спустя некоторое время разработчик получает отзыв от разгневанного пользователя из Нью-Йорка: тот пишет, что уведомления приходят вовсе не в полдень, а в 7 утра. Нью-Йорк находится в часовом поясе ETC (Eastern Time Zone), то есть UTC -05:00.
Для разных часовых поясов нельзя просто ставить таймер, отталкиваясь от времени на сервере. Разработчик добавил выбор часового пояса при регистрации и переписал планировщик так, чтобы он стартовал для каждого пользователя раньше или позже на несколько часов в зависимости от данных, которые человек ввёл при регистрации.
Кажется, теперь все работает хорошо — но в ноябре разработчику начали писать пользователи из Москвы и жаловаться на уведомления, которые приходят в 11 утра. Дело в том, что в Великобритании время переводится с зимнего на летнее, а в России — нет. К тому моменту разработчик начал нервничать и решил просто записать в коде, что если пользователь из России, то делать переход во времени для него не нужно. Приложение тем временем только набирало популярность.
В марте, разработчику пишут из Сиднея — им уведомление приходит в два часа дня. Все потому, что в южном полушарии в марте происходит переход не на летнее время, а на зимнее. Разработчику не оставалось ничего иного, как дополнить свой код информацией о том, что в южном полушарии надо переводить время наоборот. Приложение уже стало популярным во всём мире и на разработчика посыпались претензии со всех уголков света. Например, пользователи из Палестины писали жалобы каждый раз, как их правительство переводило часы с летнего времени на зимнее, а случается это каждый раз в разное время. А ещё дело было в 2011 году, и 29 декабря пользователи из Самоа дружно пересекли линию перемены даты, совершив скачок в будущее и очутившись 31 декабря 2011 года. Еще через год разработчику позвонили из Международной службы вращения Земли и сообщили, что ему надо поправить приложение, потому что в этом году будет на одну секунду больше, чем в прошлом из-за добавления дополнительной секунды. Записывать все это в код — совершенно безумная идея. Поэтому разработчик закрыл проект и больше никогда не писал код.
Что бы мог сделать разработчик, чтобы избежать такого печального финала? Все просто — достаточно было учесть опыт тех, кто уже решал эту проблему и выкладывал решение в открытый доступ. В каждом языке программирования есть как минимум одна библиотека, которая умеет работать с человеческим временем и часовыми поясами. Более того, существует база данных, в которой хранится информация о локальном времени, в том числе в исторической перспективе.
Что бы мог сделать разработчик:
Существует много сценариев, при которых крайне важна последовательность событий, на которую не могут повлиять ни изменения часовых поясов, ни переход на летнее время.
Предположим, нужно написать сервис, обслуживающий банковские транзакции. Его функционал крайне простой: один пользователь отправляет другому деньги банковским переводом, любой из них может получить информацию обо всех переводах. В данном случае нужно хранить информацию о том, кто, кому, когда и сколько денег перевёл. При этом показывать эту информацию нужно в локальном времени пользователя при условии, что транзакции в любом часовом поясе идут одна за другой.
Учтём опыт разработчика из примера выше и не будем описывать время самостоятельно, а используем библиотеку. Тут мы столкнемся с рядом вопросов:
И лучшим решением будет не делать ничего. Не нужно учитывать временные зоны клиентов, не нужно учитывать переходы на летнее и зимнее время, не нужно учитывать переезды пользователей. Когда нужно абсолютное время, достаточно использовать UTC.
Время в UTC поддерживается почти всеми библиотеками и почти всеми база и данных. Используя UTC, мы можем получать транзакцию в локальном времени пользователя, переводить это время в UTC и сохранять у себя. И транзакции всегда будут идти одна за другой.
Правда, существует проблема: UTC учитывает дополнительные секунды, а значит, в какой-то момент мы можем получить такие транзакции:
Скорее всего программа, работающая с UTC, не сможет сказать, что произошло раньше — вторая транзакция, третья или четвертая. Google, которой очень важна абсолютная последовательность событий во времени, очень интересно решила эту проблему. В своих серверах точного времени они добавляют дополнительную секунду не за раз, а равномерно распределяют её в течение дня в начале года. Это называется leap smear. Таким образом, раз в год, когда астрономы объявляют дополнительную секунду, в течение суток сервера точного времени Google немного отстают от UTC, чтобы набрать дополнительную секунду.