Atomik Islemler
Bugun ki yazimizda ARM islemcilerde bulunan atomik islemler yani interrupt veya baska bir kaynaktan (multi-thread, multi-processor) kesilemeyen/etkilenmeyen islerin nasil yapilabilecegine bakacagiz.
Atomik Islem Nedir ?
Basitce atomik islem demek herhangi bir sekilde kesilmeden devam edebilen ve/veya islemin etkilenmeden yuruyebilecegi tarzda herhangi bir dis kaynaga ihtiyac duymaksizin yapilan islemlere denir.
ARM Mimarilerinde Atomik Degiskenlere Giris
Ornegin ARM mimarisinde tek islemcili bir sayiya asagida ki gibi bir atama yapalim.
1 2 |
int x; x = 5; |
https://godbolt.org/z/jdPfrW9e6
Yukarida paylastigim linkte gorulebilecegi gibi x = 5 operasyonu icin derleyici soyle bir kod uretmekte.
1 2 |
movs r3, #5 str r3, [r7, #4] |
ARM Assembly konusuna hakim olmayanlar icin basitce r3 registerina 5 yazip, sonrasinda bu degeri tek bir instruction (str: store) degiskenin icerisine yazmakta yani 1 cekirdekli bir MCU da bu islem sirasinda int. alinamayacagi icin (inst. yurutme sirasinda islemci interrupt alamaz…) bu islem atomiktir diyebiliriz. Peki birden cok islemciniz varsa evet artik yavasca atomik degiskenlerin farkliligina yavas yavas yaklasmaya basladik 🙂
Bugune kadar bir cok islemci mimarisi bu handikapi cozebilmek adina assembly komutlarina ekleme yapmistir x86 mimarisinde LOCK komutu ile isleminizi atomik hale getirebilirsiniz. Nitekim x86 CISC mimari bir islemci oldugu icin tek instruction da istediginiz degiskeni diger corelardan izole ederek okuyup, degistirebilirsiniz.
Fakat gelelim ARM mimarisine, ARM mimarisi RISC tabanli yani komplike olmayan bir komut setine sahip oldugu icin mimariyi tasarlayanlar dogrudan hafiza uzerinde islem yapmamiza izin vermemektedir. Yani,
pseudo-assembly bir kod ile anlatacak olursak
1 2 3 4 5 6 7 |
#x86 mimarisi icin +1 yapma INCREMENT [&x] #ARM Mimarisi icin +1 yapmak LOAD Rx, [&x] INCREMENT Rx STORE [&x], Rx |
Yani bir degiskeni arttirabilmek adina once degiskeni islemci icindeki registera (yazmac) cekip sonrasinda registerda arttirip sonra tekrar yerine koyabiliriz. Bu surecte tek yapmak istedigimiz x degiskenini 1 arttirmak, peki araya interrupt girerse o zaman kodumuz neye donusecek ona bakalim.
1 2 3 4 5 6 7 8 9 10 11 |
=======x baslangic degeri 5 kabul edelim========= LOAD Rx, [&x] #x degiskeni 5 olarak Rx e yuklendi ------------Interrupt Context-----> ... LOAD Rn, [&x] INCREMENT Rn STORE [&x], Rn #x degiskeni 6 olarak x e yuklendi ... -------------Interrupt Sonu-------> INCREMENT Rx #Rx registerinda degisken degerimiz 5 idi, fakat hafizada 6 oldu. Akista burayi kacirdigimiz icin artik cok gec :( STORE [&x], Rx #x degiskeni tekrar 6 olarak hafizaya yazildi... |
Hayalimiz x degiskenini 1 arttirmak idi fakat gelen interrupttada 1 arttirildi fakat biz ezerek tekrardan 6 yaptik yani bu akista hayalimiz islem sonunda 7 gormek iken 6 ile basbasa kaldik. Peki cozumlerimiz ne olabilir ?
1- x’i arttirirken tum interruptlari kapatmak ?
2- x’e bizden baskasi erismis ise tekrar okuyup arttirmak ?
Interruptlari Devre Disi Birakmak
Bahsi gecen akista interruptlari devre disi birakip x degiskenini arttirmak tek cekirdekli bir islemcide kesin bir cozum olacaktir. Bu islemi yapabilmek adina 2 adet instruction (komut) daha eklersek asagida ki gibi degiskenimizin atomik olmasini garanti edebiliriz. Peki devamli surette tum akisimizda bu degiskeni takip edip eristigimiz her yere interrupt koymaya calismaya deger mi ? Ya da multicore (cok islemcili) bir ortamda calisiyorsaniz ne yapilacak, siz asenkron oldugunuz icin diger islemciyi bu erisim sirasinda durdurmaya mi calisacaksiniz ? Tabii ki her sey tercih meselesi lakin daha kolayini bir sonraki alt basligimizda inceleyecegiz.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
=======x baslangic degeri 5 kabul edelim========= INTERRUPTS DISABLE #interruptlar kapatildi LOAD Rx, [&x] #x degiskeni 5 olarak Rx e yuklendi INCREMENT Rx #Rx registerinda degisken degerimiz 5 idi. STORE [&x], Rx #x degiskeni 6 olarak hafizaya yazildi... INTERRUPTS ENABLE #interruptlar acildi ------------Interrupt Context-----> ... LOAD Rn, [&x] INCREMENT Rn STORE [&x], Rn #x degiskeni 7 olarak x e yuklendi ... -------------Interrupt Sonu-------> |
Erisim Kontrollu Atomik Degisken Olusturma
ARM icin linked access komutlari bulunmaktadir. Bu komutlar en yalin haliyle LDREX, STREX, CLREX komutlaridir.
LDREX: LOAD exclusive
STREX: STORE exclusive
CLREX: Clear exclusives
Bu komutlarin ozelligi ise su sekildedir ve birlikte kullanilirlar.
1 2 3 |
LDREX Rx, [Rn] # Rn registerinda adresi bulunan degiskeni Rx reg. ine oku ADD Rx, 1 # korumasiz bir 1 arttirma islemi STREX Rx, [Rn] # Rx registerini Rn'in gosterdigi adrese yuklemeyi dene |
Burada ki store komutumuz STREX degisik ozellikte bir komuttur, diger store komutlarinin aksine yuklemeyi dener peki neye gore dener ?
Erisim Kontrolculeri
Islemci paketimizin icerisinde local ve global access (erisim) monitorleri bulunur. Bu monitorlerin yaptigi temel yukarida ki kodda bulunan erisimleri kontrol etmeye yarar. Yani asagida ki gibi calisir.
Global ve local access monitorleri sadece cok cekirdekli islemcilerde bulunurken, tek cekirdekli islemcilerde yalnizca local access monitoru bulunur.
Gercek Bir Erisime Hazir Olun!!!
1- LDREX Rx, [Rn] : Rn adresini belirli bir range (adres araligi) icerisinde monitorde exclusive olarak isaretle… (Range tamamen implemantasyona ozeldir yani MCU ureticisi belirler)
2- STREX Rd, Rx, [Rn]: isaretlenen bolgenin exclusive ozelligini bozan olmadiysa Rx i yukle. Islemin sonucunu Rd registerina yukle
Peki yukleme islemi basarili olup olmamasi nereden anlasilir ?
Ornek olarak Cortex M4 mimarini ele alalim. Bu mimaride genellikle tek cekirdekli islemci setleri kullanilmaktadir.
1 2 3 4 5 6 7 8 |
.L2: ldrex r2, [r3] add r0, r2, r1 strex ip, r0, [r3] #1. parametre geri donus yani erisim basari durumu, 2. parametre yuklenecek register, 3. parametre degisken adresimiz cmp ip, #0 #geri donus degeri kontrolu bne .L2 #basarisiz ise tekrar basla dmb ish #memory barrier |
https://godbolt.org/z/s1xxfT8sq
Cortex M mimarisine ozel olarak Cortex A dan farkli olarak interrupt durumunda herhangi bir aksiyon alinmadan CLREX komutu calistirilir bu komut local monitorde ki tum exclusive erisimleri “Open Access” olarak isaretler. Yeni yukarida ki dongumuzde en basa donup tekrar bir okuma yapip islemi yeniden yapmak durumunda kaliriz.
Cok cekirdekli islemcilerde ise monitor sayisi artar ve corelar arasi monitor olan global access monitoru de devreye girer. Eger baska bir cekirdek bizim adresimize erisim saglamissa tekrardan ayni donguye gireriz.
Artik gelelim C ile bu isleri yapmaya yani anlattigimiz kismi gerceklestirmeye…
C11 Standardi ile Gelen _Atomic
C11 standardi ile gelen _Atomic anahtar kelimesi, derleyicimizin bizlere yaptigi yegane guzelliklerden biri 🙂
Yukarida anlattigimiz tum bu donguyu asagida ki gibi kurabiliriz.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <stdatomic.h> struct usual_s { volatile int _Atomic b; }; static struct usual_s a_var = {.b = ATOMIC_VAR_INIT(0)}; void func() { a_var.b--; } void interrupt() { a_var.b++; } |
Assembly ciktisinda goruldugu uzere func isimli tasklarda kosabilen fonksiyonumuz interrupt degiskeni degistirdigi zaman tekrardan degiskeni okuyarak azaltmaya calisiyor.
Diyelim ki bir interrupttan uretilen sayilar olsun ve 2 farkli threadde kosan kodumuz bunu karsilikli olarak tuketmeye calissin.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
#include <stdatomic.h> struct usual_s { volatile int _Atomic b; }; static struct usual_s a_var = {.b = ATOMIC_VAR_INIT(0)}; void thread1() { int old_value = atomic_load(&a_var.b); int new_value; while (old_value > 0) { new_value = old_value - 1; if (!atomic_compare_exchange_strong(&a_var.b, &old_value, new_value)) { /*degiskenimiz bizim okudugumuz deger ile ayni degil, araya baska yapilar girdi*/ old_value = atomic_load(&a_var.b); /*tekrardan okuma yapiliyor*/ continue; } break; /*islem basarili*/ } } void thread2() { int old_value = atomic_load(&a_var.b); int new_value; while (old_value > 0) { new_value = old_value - 1; if (!atomic_compare_exchange_strong(&a_var.b, &old_value, new_value)) { /*degiskenimiz bizim okudugumuz deger ile ayni degil, araya baska yapilar girdi*/ old_value = atomic_load(&a_var.b); /*tekrardan okuma yapiliyor*/ continue; } break; /*islem basarili*/ } } void interrupt() { a_var.b++; } |
Yukarida ki yapida interrupt 1 arttirilan bir degisken yardimiyla 2 thread birbirlerinin ayagina dolanmadan istenilen miktarda calisacaktir. Yani 10 kez interrupt aldiysak, 2 threadden cagrilan yerler toplamda sadece 10 kez calismasini garanti altina almis olduk…