farhad shiri
farhad shiri
خواندن ۱۲ دقیقه·۳ سال پیش

روتین اتمیک با ضمانت اجرای سطح پایین توسط پردازشگر ARM


تقریبا همه اونهایی که در حوزه نرم افزار فعال هستند، به هرحال یکطوری با مباحث Thread و برنامه نویسی Multi-threading و همچنین به خیلی از چالش ها و اتفاقاتی که به سختی میشه بدرستی پیشی بینی شان کرد، برخورد داشتید.

بنابراین هرچقدر هم که درباره چالش های این مبحث، مستندات فنی و تجربه کسب کنیم بازهم واقعا یکی از پیچیده ترین اتفاقاتی که در بستر سیستم عامل و پردازشگر رخ میده همین مبحث Multi-threading هستش.

به همین علت یک از روشهایی که اخیرا در یکی از پروژه هایی که با پردازشگر معماری ARM، در بستر امبدد لینوکس بود را میخواهم شرح بدم که روشی که من برای حل این مسئله بکار بردم چه بود، امیدوارم مفید فایده قرار بگیرد



برای قسمتی از یک دیوایس سخت افزاری که با معماری ARM طراحی شده است، باید یک ساعت Real-Time نوشته میشد که بر روی واحد LCD این ساعت که محتوی ثانیه شمار و دقیقه شمار و ساعت شمار بود، باید به درستی زمان را نمایش میداد، البته صرفا برای نمایش زمان نبود خودش جزئی از یک بخش کلی تر بود که بر حسب بیزنس قرار بود فرآیندهای دیگه ای را هم انجام بده.

بنابراین یکی از چالش های اصلی این بود که اصلا نمیتونستیم این زمان شمار را در نخ اصلی برنامه اجرا کنیم چرا که بر روی همه واحدهای دیگه سخت افزار تاثیر منفی داشت بنابراین با توجه به Latency که وجود داشت تصمیم گرفتم که کلا این زمان سنج را در یک Thread مجزا قرار بدیم.

با توجه به اینکه ما زمان را از خود پردازشگر دریافت میکردیم برای محاسبه و تبدیل به زمان محلی این عملیات باید به صورت اتمیک انجام میشد. و نکته بسیار مهم برای ما این بود که باید هر بار اون پیکسل هایی که بر روی واحد نمایشگر وظیفه نمایش ساعت را داشتند به روز میکردیم بطوریکه نمایش ساعت بر روی دستگاه با یم ساعت آنالوگ دقیقا برابر باشد و تاخیری نداشته باشد.

بنابراین کل این فرآیند دریافت زمان از پردازشگر ، تبدیل ساعت محلی ، نمایش در واحد پردازشگر اونهم فقط بصورتیکه همون قسمت Refresh بشه، در یک Thread والبته چون معماری ما از نوع SOA بود، در یک پردازه فرزند که با پردازه اصلی Fork شده بود، قرار دادم و خوب ظاهرا همه چیز به خوبی پیش میرفت.

ولی نکته اساسی این بود که طراحی سخت افزاری این دستگاه طوری بود که ما دیوایس های دیگه ای هم روی این سخت افزار داشتیم که هریک فرآیندهای دیگه ای را انجام میدادند، و همانطور هم گفتم چون از معماری سرویس گرا استفاده کردیم، بنابراین هر سرویس به صورت ایزوله درحال فعالیت هستند مگر زمانهایی که نیاز باشه در Memory Pool که بین همه سرویس ها مشترک هست اطلاعاتی درج کنند ویا اطلاعاتی را بخوانند.

بنابراین زمانی که سخت افزار ما در حال دریافت یک سیگنال از دیوایس سیگنال بود، دقیقا همون زمان از واحد ساعت Real time داشت اطلاعاتی برای نمایش آماده میکرد، ویا زمانی که دقیقا داشت از دیوایس WAN-4G داده دریافت میکرد، بازهم همزمان در حال نمایش ساعت هم بود.

بنابراین تداخلی که در همزمانی نمایش ثانیه ای و به روز رسانی دیوایس LCD داشتیم، در برخی از شرایط دچار کرش کردن سخت افزار میشد. توجه داشته باشید که این کرش به علت سخت افزاری بود اونهم بخاطر اینکه سخت افزار دیوایس برای سرویس گرا بودن طراحی بهینه نداشت، وبهترین جواب را در زمان Monolithic بودن داشت. بنابراین ما هیچ داده ی اشتراکی ویا یک ناحیه بحرانی که باعث کرش باشه نداشتیم بلکه بحث Data Bus , Address Bus و همچنین دستیابی به Data Mem Section,Instruction Mem Section را داشتیم که بخاطر معماری سخت افزار بود تا مشکل نرم افزار.

به همین علت برای رفع این مشکل تصمیم گرفتم که در زمانی که سرویسهای حیاتی در حال سرویس دادن هستند، واحد نمایش ساعت اون پریود زمانی را متوقف باشه، وبعد از اتمام سرویس حیاتی از همونجا که متوقف شده بود، شروع به ادامه کار بده و فرآیند خودش را تکمیل کنه.



برای حل مشکلی که در بالا اشاره شد، قطعا چندین روش وجود داره مثل Semaphore, Mutual Exclusion, Lock Monitor ویا حتی تکنیک های پیشرفته استاندارد های POSIX مثل Signal , System call که هرکدام مزایا و البته معایب خودشون را دارند، ولی روشی میخواستم که خیلی مطمئن باشه والبته در معماری ARM و همچنین در معماری ISA-ARM مثل Thumb,Thumb2,ARM32 قابلیت پیاده سازی داشته باشه. یعنی روشی را میخواستم که خود پردازشگر دقیقا به صورت سخت افزاری یک op code برای محافظت از همزمانی Data Bus,Address Bus در زمان PIPE کردن بایت کدها، اجرا کنه واین همزمانی را تضمین کنه.

که خوشبختانه دقیقا مثل معماری پردازشگرهای CISC که غالبا در معماری Von Neumann استفاده میشوند، دستوراتی مانند lock در معماری ARM وجود داره، که دقیقا به نام عملیتهای اتمیک محض که توسط پردازشگر در پایین ترین سطح ممکن تضمین میشوند، معرفی میشوند

بنابراین دنبال انتزاع سطح بالاتری از پیاده سازی عملیتهای اتمیک گشتم، که در استانداردهای 99 زبان سی معرفی شده باشند، توجه داشته باشید که به علت نوع پروژه از وابستگی های کتابخانه ای نمیتونستم استفاده کنم بنابراین باید پیاده سازی عملیات اتمیک در خود استاندارد وجود داشته باشه. که خوب متاسفانه این پیاده سازی در نسخه های جدیدتر استاندارد ایزو 11 به بعد به زبان اضافه شده اند، بنابراین به همین سادگی هم نمیتونستم کرنل را تغییر بدهم ویا حتی از یک کامپایلر به روز تر استفاده کنم که با استانداردهای جدیدتر سازگار بشم، البته نه اینکه نشدنی باشه نه بیشتر بحث زمان و پیچیدگی سیستم وهمچنین پایداری یک سیستم که لانچ شده بود مطرح هستش.

به همین علت با مطالعه رفرنس پردازشگر ARM-v7 به سری دستورات رسیدم که دقیقا برای اتمیک کردن دستورات استفاده میشدند، و میتونستم خودم با یک کد inline assembly این مشکل اتمیک کردن عملیات را مرتفع کنم بنابراین در مجموع کدهای زیر را نوشتم.

در ادامه سعی میکنم که توضیحات کدها را هم بنویسم

#define LOCK_IS_OPEN 1
#define INFINIT_LOOP 1

enum {
lock_accquire=0,lock_init=1,lock_release=2
};

int32_t atomicLockAcquire(INT32 change) {
static int32_t s_clockLock_t = LOCK_IS_OPEN;
if (change == lock_init)
atomic_dec(&s_clockLock_t);
else if (change == lock_release)
atomic_inc(&s_clockLock_t);
return(atomic_acquire_load(&s_clockLock_t));
}

void memory_barrier() {
__asm__ __volatile__ ("dmb" : : : "memory");
}

int32_t atomic_dec(volatile int32_t *io_atomic_offset) {
return atomic_add(-1, io_atomic_offset);
}

int32_t atomic_inc(volatile int32_t *io_atomic_offset) {
return atomic_add(1, io_atomic_offset);
}

int32_t atomic_acquire_load(volatile const int32_t *io_atomic_offset){
int32_t value = *io_atomic_offset;
memory_barrier();
return value;
}

//load last value from (io_atomic_offset->%4) into (prev_vlaue->%0) memory address
//add (prev_vlaue-%0) value with (in_increment->%5) value resault to (tmp_value->%1)
// memory adddress
//try and claim the lock, If the exclusive monitor permit (0) write to (lock_status->%2)
//write (1), otherwise

int32_t atomic_add(int32_t in_increment, volatile int32_t* io_atomic_offset) {
int32_t prev_vlaue, tmp_value, lock_status;
memory_barrier();
do {
__asm__ __volatile__ ("ldrex %0, [%4]\n"
"add %1, %0, %5\n"
"strex %2, %1, [%4]"
: "=&r" (prev_value), "=&r" (tmp_value),
"=&r" (lock_status), "+m" (*io_atomic_offset)
: "r" (io_atomic_offset), "Ir" (in_increment)
: "cc");
} while (__builtin_expect(lock_status != 0, 0));
return prev_vlaue;
}

اول از اینکه کدها یکم نامفهوم بنظر میرسه ، عذرخواهی میکنم چون اجازه قراردادن همه کدها را نداشتم بنابراین بخش هایی که دقیقا برای این پست هستند را جدا کردم، البته معمولا کدهای inline asm کمی پیچیدگی بیشتری دارند. بدین ترتیب کدهایی که دربالا مشخص شده اند در زمانهای مشخصی عملیات اتمیک لاک کردن را انجام میدهند وهمانطور که قبلا هم گفتم فرآیندهایی که این روتین ها را فراخوانی میکنند از thread های دیگه ای هست که در پایینتر اشاره خواهم کرد.

قسمت مهم این کد بخشی هست که ما با سری دستورات ldrex,strex که دستورات Load/Store Exclusive هستند، توانستیم مکانیزمی بنویسیم که زمانی که از thread دیگری سیگنالی بابت suspend شدن نخ مالک قفل اتمیک ارسال میکنیم، خود پردازشگر این تضمین را میده که وقتی روی اون آفست قفل اتمیک در Data Bus در حال ارسال داده هست تا در حافظه write بشه، اگر نخ دیگری درخواست load قفل را داشته باشه، پردازشگر اون انتظار را برای دریافت ویا حتی نوشتن را برای اون PIPE در نظر میگیره، وتمام این عملیات خودش کنترل میکنه، به علت اینکه توضیح این مسائل لازم داره که برخی فرآیندهای سطح پایین را توضیح داد بسیار کار سختی هست که بخواهم خیلی بهتر از این موضوع را باز کنم، بنابراین درصورتیکه مبهم بود لطفا به رفرنس اصلی پردازشگرهای ARM مراجعه نمایید. و همچنین درباره نحوه نوشتن کدهای inline assembly در گنو لینوکس هم برای درک بهتر به رفرنس خود GCC مراجعه کنید البته خیلی ساده یکسری کامنت در کد قرار داده ام ولی متاسفانه نحو دستورات inline asm در GCC خیلی پیچیده است و استنتاج اون از روی کد کار سختی هستی باید در زمان اجر و دیباگ این کدها را بررسی کنیدویا خروجی نهایی اسمبلی تولیده شده کامپایلر را که تبدیل به آبجکت فایل میشه بررسی کنید.

و بعد در هر Process ویا Thread که بخواهم اون را Suspend کنم تا یک عملیات دیگه انجام بشه به صورت زیر میتونم استفاده کنم، البته به علت پیچیدگی و محرمانگی فقط بخش هایی از کدها را آوردم که موضع روشن تر باشه.

روتینی که وظیفه نمایش ساعت Real را داره...

void *rtClockRoutine(void *i_args) {
/*get real date and time from device
* format data is miladi
* */
while (INFINIT_LOOP) {
/* check the lock_store when is locked by other thread!. */
int32_t lock = atomicLockAcquire(lock_accquire);
/*show the clock until the lock_store is open! */
while (lock == LOCK_IS_OPEN) {
DWORD l_dwCurrTime = dmgBasic_GetTime();
BYTE l_rtClockStr[LENGTH_OF_REALTIME_STR] = {0};

snprintf((char*)l_rtClockStr, LENGTH_OF_REALTIME_STR,
"%02d:%02d:%02d", utlGetHour(l_dwCurrTime),
utlGetMinute(l_dwCurrTime), utlGetSecond(l_dwCurrTime));

lcd_display_color(0, 0, FONT_SIZE12 | DISP_RIGHTAT | DISP_INVCHAR,
DISP_COLOR_BLACK, HEADER_BACK_COLOR, (const char*)l_rtClockStr);
lcd_refresh();
/* change the idle page information in 23:59:59 */
if (strcmp((const char*)l_rtClockStr, "23:59:59") == 0) {
/* not need send the signal to UI thread,
* refresh immediate idle page. */
uiSrvShowIdlePage(TRUE);
/* Because the loop can be run more than once,
* this delay is one second to prevent re-run */
sleep(1);
}
/* [IMPORTANT]
* this line transposition is very important!
* not change this line position */
lock = atomicLockAcquire(lock_accquire);
}
}
return ((void *)FALSE);
}

بنابراین با این روتین ساعت را نمایش میدیم، وبا روتین دیگه ای که در پایین آوردم از یک پردازه دیگه میتونیم این thread را متوقف ویا مجددا استارت کنیم.

روتین فرآیند بررسی یک سیگنال دریک دیوایس دیگه...

void *ReadForCardRoutine(void *i_args) {
while (INFINIT_LOOP) {
//countOfDetect++;
if (...) {
/* because real-time clock thread refresh LCD device,
* to avoid device crashing, real-time clock thread is locked*/
atomicLockAcquire(lock_init);
/* send the signal to the monitor event handler,
* is data stream read by device and ready to process by handler routine. */
RAISE_EVENT(g_monitor_event_t, SIGCARD);
/* this sleep thread operation and waiting Which has several advantages.
* 1- wake up when event handler thread operation, is complete done.
* 2- This loop usually takes several cycles to execute,
* so the kernel signal buffer is cleared and the signal repeated is not processed.
* 3- when wake'd up this thread, ready again for get new signal .
*/
... (my stuff code)
atomicLockAcquire(lock_release);
}
}
return ((void *)FALSE);
}

همانطور که مشاهده کردید، بخشی هایی از کدها را میشد خیلی ساده تر نوشت، اگر با استانداردهای جدیدتر استفاده میکردیم. بنابراین در برخی از مواقع استفاده از تکنیکهای اینچنینی باعث تجربه فنی مضاعفی میشه که معمولا در استفاده از کتابخانه هاو یا حتی استانداردهای جدیدتر این تجربه کمتر میشه، چرا که اونها اجازه نمیدهند که در عمق تکنولوژی شیرجه بزنید تمام این کار را خودشون انجام میدهند وبنابراین شاید خیلی کمتر درگیر استفاده از چنین تکنیکهایی بشیم که باعث عمیق تر شدن دانش میشوند

با سپاس از وقتی که برای مطالعه گذاشتید، بسیار خوشحال میشم در صورتیکه نظری داشتید بامن درمیان بگذارید

با سپاس - فرهاد شیری


gcclinux
یک توسعه دهنده نرم افزار
شاید از این پست‌ها خوشتان بیاید