Оперативната памет на вашия компютър лагва на всеки 7,8 μs

12
3688

Взето от Every 7.8μs your computer’s memory has a hiccup

 


По време на моето посещение в Музея по компютърна история в Маунтин Вю, видях древен образец на феритна памет.

Не знам, как точно работи тази памет – дали феритните пръстени се въртят (не, не се въртят) и защо през всеки пръстен минават три проводника? Усетих, че не знам и как точно работи съвременната динамична оперативна памет.

Известно е, че в динамичната RAM, всеки бит се запаметява във вид на заряд (или липсата на заряд) в миниатюрен кондензатор в DRAM чипа. За да не се изгубят записаните данни се налага тяхното непрекъснато обновяване, познато повече като опресняване на динамичната памет. Процесът на опресняване всъщност е четене на всеки бит и след това неговият запис – битовете се прочитат и презаписват. По време на опресняването, паметта е заета и не може да изпълнява стандартните операции четене и запис.

И веднага възникна въпросът, а дали не е възможно по чисто софтуерен път да измерим това забавяне на DRAM паметта, по време на което се извършва опресняването?

Какво ще използваме

Всяка DIMM плочка е съставена от клетки, подредени в редове и колони. Редовете и колоните от своя страна образуват страници на паметта, а всички клетки са разделени в няколко области.

Конфигурацията на паметта в компютърната система може да бъде проверена например с командата decode-dimms. Ето един пример:

$ decode-dimms
Size                                       4096 MB
Banks x Rows x Columns x Bits              8 x 15 x 10 x 64
Ranks                                      2

Но съвсем не е необходимо да се задълбочаваме в работата на цялата плочка памет. За нашата задача е достатъчно да разберем работа на само една клетка, понеже ни интересува само процеса по опресняването.

Ще се ръководим от два авторитетни източника на информация:

Всеки бит в динамичната памет с произволен достъп задължително трябва да се опреснява. Обикновено това става на всеки 64 ms (параметърът Static Refresh). Това е твърде скъпа операция за една компютърна система, която ще погълне твърде много ресурси. За да се избегне едно дълго спиране на работата на всеки 64 ms, процесът е разделен на 8192 по-малки операции на опресняването. При всяка една от тях контролерът на паметта изпраща команди за опресняването на DRAM. След получаването на тази инструкция, чипът обновява 1/8192 от своите клетки. Да пресметнем: 64 ms/8192 = 7812,5 ns или 7,81 μs.

Може да кажем, че:

  • Командата за обновяване се изпълнява на всеки 7812,5 ns. Тя носи името tREFI
  • Процесът по прочитането и презаписа на клетките отнема известно време, след което чипът отново може да извършва стандартните четене и запис. Това е параметърът tRFC, който при много чипове е 75 ns, като в документацията на Micron tRFC е 120 ns.

Има и други фактори. Ако паметта е гореща (над 85°C), кондензаторите се разреждат по-бързо, времето за съхранение на данните пада, и времето на Static Refresh намалява двойно до 32 ms. Съответно, tREFI пада до 3906,25 ns.

Типичният DRAM чип е зает с опресняването през немалка част от своето време – от 0,4 до 5%. Освен това, значителна част от консумацията на електрическа енергия на DRAM чиповете се дължи именно на процеса на опресняване.

Има и друго: по време на опресняването се блокира работата на целия чип. Тоест, всеки един бит на динамичната памет е блокиран за над 75 ns на всеки 7182 ns. Хайде да го измерим.

Подготовката на експеримента

За да можем да измерим тези операции с наносекундна точност е необходим много плътен цикъл. Това може да стане с помощта на програмния език C и изглежда по следния начин:

 

  for (i = 0; i < ...; i++) {
		// Perform memory load. Any load instruction will do
		*(volatile int *) one_global_var;

		// Flush CPU cache. This is relatively slow
		_mm_clflush(one_global_var);

		// mfence is needed, otherwise sometimes the loop
		// takes very short time (25ns instead of like 160). I
		// blame reordering.
		asm volatile("mfence");

		// Measure and record time
		clock_gettime(CLOCK_MONOTONIC, &ts);
    }

Целият сорс код може да бъде взет от GitHub

Кодът е съвсем опростен: четене на паметта, изчистване на данните от кеша на централния процесор, измерване на времето.

В моя компютър се появи следната информация:

# време, дължина на цикъла
3101895733, 134
3101895865, 132
3101896002, 137
3101896134, 132
3101896268, 134
3101896403, 135
3101896762, 359
3101896901, 139
3101897038, 137

Обикновено цикълът се изпълнява за около 140 ns, но от време на време скача до около 360 ns. А понякога се появяват странни резултати от над 3200 ns.

За съжаление, явно в тези данни има много много шум и е твърде трудно да се забележи задръжката, свързана с циклите по опресняване на DRAM паметта.

Бързото преобразование на Фурие

В един момент бях осенен от интересна мисъл. Всъщност ние искаме да регистрираме събитие с фиксирана дължина и е логично да подадем данните в FFT алгоритъм (Fast Fourier transform или бързото преобразование на Фурие). По този начин ще научим основните честоти.

Съвсем не съм първият, досетил се за това. Марк Сибърн с гениалната уязвимост Rowhammer използва тази техника още през 2015 година. Но дори със сорс кода на Марк, всичко се оказа доста сложно, но намерих начин.

Първоначално е необходима подготовка на данните. FFT работи с входни данни с един и същ интервал за тяхното получаване. След това трябва да се изреже шума. Аз постигнах най-добър резултат при следните обработки на входния сигнал:

  • Малките значения (под 1,8 от средното) на итерациите на цикъла се премахват и се заменят с нули. Не искаме шум
  • Всички останали показания се заменят с единици, понеже нас не ни интересува амплитудата на задръжката, предизвикана от някой външен шум

Спрях се на стъпка на дискретизация 100 ns, но е подходящо всяко число до честотата на Найкуист – двойно по-високата от очакваната честота

Алгоритъмът изглежда по следния начин:

UNIT=100ns
A = [(timestamp, loop_duration),...] 
p = 1
for curr_ts in frange(fist_ts, last_ts, UNIT):
    while not(A[p-1].timestamp <= curr_ts < A[p].timestamp):
        p += 1
    v1 = 1 if avg*1.8 <= A[p-1].duration <= avg*4 else 0
    v2 = 1 if avg*1.8 <= A[p].duration <= avg*4 else 0
    v = estimate_linear(v1, v2, A[p-1].timestamp, curr_ts, A[p].timestamp)
    B.append( v )

Този алгоритъм генерира един доста скучен вектор:

[0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0,
 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...]

Всъщност, векторът е голям – около 200 000 данни. Но именно с тези данни вече може да се използва FFT!

C = numpy.fft.fft(B)
C = numpy.abs(C)
F = numpy.fft.fftfreq(len(B)) * (1000000000/UNIT)

Изглежда лесно, нали? Получават се два вектора:

  • Векторът C съдържа комплексните числа на честотните компоненти. Тук не се интересуваме от комплексните числа и можем да ги изгладим с командата abs()
  • Векторът F съдържа маркерите, показващи какъв честотен промеждутък в кое място на вектора C се намира. Трябва да го нормализираме до херц чрез умножение на честотата на дискретизация.

Резултатът образува следната диаграма:


Оста Y няма никакви измервателни единици, понеже игнорирахме времето на задръжката. Но съвсем ясно се виждат честотните пикове. Да ги разгледаме по-отблизо:


Ясно се виждат първите три пика. От тях можем да извлечем базовите честоти:

127850.0
127900.0
127950.0
255700.0
255750.0
255800.0
255850.0
255900.0
255950.0
383600.0
383650.0

Пресмятаме: 1000000000 (ns/s)/ 127900 (Hz) = 7818,6 ns.

Да, първият пик на честотата е именно това, което търсехме и корелира с времето на опресняване.

Останалите пикове на честоти 256 KHz, 384 KHz и 512 KHz са хармониките на нашата базова честота 128 KHz. Това е стандартен страничен ефект при използването на FFT с правоъгълни импулси.

За облекчаване на експеримента, правим скрипт за командния ред. Ето как изглежда той на моя сървър:

~/2018-11-memory-refresh$ make
gcc -msse4.1 -ggdb -O3 -Wall -Wextra measure-dram.c -o measure-dram
./measure-dram | python3 ./analyze-dram.py
[*] Verifying ASLR: main=0x555555554890 stack=0x7fffffefe2ec
[ ] Fun fact. I did 40663553 clock_gettime()'s per second
[*] Measuring MOVQ + CLFLUSH time. Running 131072 iterations.
[*] Writing out data
[*] Input data: min=117 avg=176 med=167 max=8172 items=131072
[*] Cutoff range 212-inf
[ ] 127849 items below cutoff, 0 items above cutoff, 3223 items non-zero
[*] Running FFT
[*] Top frequency above 2kHz below 250kHz has magnitude of 7716
[+] Top frequency spikes above 2kHZ are at:
127906Hz    7716
255813Hz    7947
383720Hz    7460
511626Hz    7141

Да отбележа, че кодът не е съвършен, При наличие на проблеми се препоръчва изключването на Turbo Boost.

След този експеримент можем да направим два важни извода.

  • Видяхме, че данните от ниско системно ниво трудно се анализират и са доста шумни и се налага използването на бързото преобразование на Фурие с незначително филтрирани данни.
  • Но най-главното е, че показахме точното измерване на хардуерен процес в потребителска среда. Именно този метод доведе до разкриването на уникалната уязвимост Rowhammer и на практика е реализиран в атаките Meltdown/Spectre и отново използван при новата версия на Rowhammer за ECC паметите.

 


Разбира се, това са основите и много неща останаха извън рамките на тази статия. Темата е добре развита в следните материали:

И накрая, ето едно много добро обяснение на работата на феритната памет в PDP-11

 


*Допълнителните материали, изброени малко по-горе, ще разгледаме в следващите статии

12
ДОБАВИ КОМЕНТАР

avatar
10 Коментари
2 Отговори на коментарите
11 Последователи
 
Коментарът с най-много реакции
Най-горещият коментар
  Абонирай се  
нови стари оценка
Извести ме за
Sasser
Sasser

Можело и полезни статии да пускате !

ngc-bg
ngc-bg

Чудесно написана/преведена статия. Би била полезна на всички – от начинаещи до позабравили основите вече.
Поздрави!

Биткойн = Балон = Пирамида
Биткойн = Балон = Пирамида

Светът вече не е същия след тази информация.

Иво
Иво

Има програми като Аida64 където на теста за RAM паметта ми показва за мойта че работи на 65ns. Теста се казва Закъснение на Паметта. Ето ми го лаптопа:
(Asus Vivobook Pro N580VD): CPU i5 7300HQ ^ RAM 32GB DDR4 2400Mhz ^ Nvidia GTX1050 4GB ^ SSD Crucial MX300 1TB + 250GB NVMe Samsung 970Evo

Димитър Димитров
Димитър Димитров

Страхотна статия. Беше много полезна

Благодаря ви

reldark
reldark

Можело и хубави статии да пишете. Браво.

Иво
Иво

Интересна и полезна статия. Благодаря и браво за превода.

Тони
Тони

Браво много добра статия. Да не звучи като мрънкане, но за мен щеше да е хубаво да започне с „наскоро попаднах на една статия…“ от моите опити постигнах еди какво си и тн. И още нещо трябва да се пипне мерните единици един път говорим за микросекунди след това за наносекунди.

Стефан
Стефан

Не виждам проблем с нано- и микросекундите, мисля че е ясно написано.
Единствената неточност е че целия чип не се блокира по време на опресняването, а само 1 „банка“ от него, през което време работата с останалите банки от паметта продължава нормално. Разделянето на банки спомага да се избегнат и други подобни изчаквания при работа с DRAM.

номад
номад

Какъв е смисъла от тази информация(каква е реализацията от подобен интерес)? Значи по добре да се забием в дълбокото с безсмислени анализи по темата отколкото да се научим да напишем 5 реда полезен код за някой успешен СОБСТВЕН икономически проект в интерес на хората. Това е психиката и ценностите по нашите геогравски ширини, не е важно какво можеш а да знаеш как се прецакават тези които могат. Много полезна „статия“!

ngc-bg
ngc-bg

Смисъла на подобни статии е в това, че научаваш детайли по тема, която най-общо казано, ти помага да бъдеш по-добър софтуерен/хардуерен инженер. Ежедневното обогатяване с информация е безценно за инженера, а в момента живеем в златно време за това. Преди трябваше да ровиш в дебелите книги и справоочници и като постигнеш нещо, бе трудно да покажеш на хората наоколо какво си открил/направил/научил. А и в посочената статия има повече от 5 реда интересен код. Не толкова като синтаксис/език, а по-скоро като мислене и намиране на решение в ситуация. Та нали това в е основата на всяко едно обучение?! Явно си… Виж още »

Niko
Niko

kato povdignesh chestotata na napametta i se opeche shte raboti kato raketa i sledvashtia mesec kopyvash nova pamet ili si skapvash celia komputar i otiva vav kofata za bokluk.