當前位置:首頁 > 嵌入式 > 嵌入式分享
[導讀]今天談?wù)刲inux中常見并發(fā)訪問的保護機制設(shè)計原理。這既可以考察面試者對鎖的原理的理解,又可以考察面試者編程技能)。我們拋開linux中匯編代碼。用C語言為大家呈現(xiàn)背后實現(xiàn)的原理。同時,文章中的代碼都沒有考慮并發(fā)情況(例如某些操作需要原子性,或者數(shù)據(jù)需要保護等)。

今天談?wù)刲inux中常見并發(fā)訪問的保護機制設(shè)計原理。這既可以考察面試者對鎖的原理的理解,又可以考察面試者編程技能)。我們拋開linux中匯編代碼。用C語言為大家呈現(xiàn)背后實現(xiàn)的原理。同時,文章中的代碼都沒有考慮并發(fā)情況(例如某些操作需要原子性,或者數(shù)據(jù)需要保護等)。

注:部分代碼都是根據(jù)ARM64架構(gòu)匯編代碼翻譯成C語言并經(jīng)過精簡(例如:spin lock、read-write lock)。也有部分代碼實現(xiàn)是為了呈現(xiàn)背后設(shè)計的原理自己編寫的,而不是精簡linux中實現(xiàn)的代碼(例如mutex)。

自旋鎖(spin lock)

自旋鎖是linux中使用非常頻繁的鎖,原理簡單。當進程A申請鎖成功后,進程B申請鎖就會失敗,但是不會調(diào)度,原地自旋。就在原地轉(zhuǎn)到天昏地暗只為等到進程A釋放鎖。由于不會睡眠和調(diào)度的特性,在中斷上下文中,數(shù)據(jù)的保護一般都是選擇自旋鎖。如果有多個進程去申請鎖。當?shù)谝粋€申請鎖成功的線程在釋放的時候,其他進程是競爭的關(guān)系。因此是一種不公平。所以現(xiàn)在的linux采用的是排隊機制。先到先得。誰先申請,誰就先得到鎖。

原理

舉個例子,大家應(yīng)該都去過銀行辦業(yè)務(wù)吧。銀行的辦事大廳一般會有幾個窗口同步進行。今天很不巧,只有一個窗口提供服務(wù)?,F(xiàn)在的銀行服務(wù)都是采用取號排隊,叫號服務(wù)的方式。當你去銀行辦理業(yè)務(wù)的時候,首先會去取號機器領(lǐng)取小票,上面寫著你排多少號。然后你就可以排隊等待了。一般還會有個顯示屏,上面會顯示一個數(shù)字(例如:"請xxx號到1號窗口辦理"),代表當前可以被服務(wù)顧客的排隊號碼。每辦理完一個顧客的業(yè)務(wù),顯示屏上面的數(shù)字都會增加1。等待的顧客都會對比自己手上寫的編號和顯示屏上面是否一致,如果一致的話,就可以去前臺辦理業(yè)務(wù)了?,F(xiàn)在早上剛開業(yè),顧客A是今天的第一個顧客,去取號機器領(lǐng)取0號(next計數(shù))小票,然后看到顯示屏上顯示0(owner計數(shù)),顧客A就知道現(xiàn)在輪到自己辦理業(yè)務(wù)了。顧客A到前臺辦理業(yè)務(wù)(持有鎖)中,顧客B來了。同樣,顧客B去取號機器拿到1號(next計數(shù))小票。然后乖乖的坐在旁邊等候。顧客A依然在辦理業(yè)務(wù)中,此時顧客C也來了。顧客C去取號機器拿到2號(next計數(shù))小票。顧客C也乖乖的找個座位繼續(xù)等待。終于,顧客A的業(yè)務(wù)辦完了(釋放鎖)。然后,顯示屏上面顯示1(owner計數(shù))。顧客B和C都對比顯示屏上面的數(shù)字和自己手中小票的數(shù)字是否相等。顧客B終于可以辦理業(yè)務(wù)了(持有鎖)。顧客C依然等待中。顧客B的業(yè)務(wù)辦完了(釋放鎖)。然后,顯示屏上面顯示2(owner計數(shù))。顧客C終于開始辦理業(yè)務(wù)(持有鎖)。顧客C的業(yè)務(wù)辦完了(釋放鎖)。3個顧客都辦完了業(yè)務(wù)離開了。只留下一個銀行柜臺服務(wù)員。最終,顯示屏上面顯示3(owner計數(shù))。取號機器的下一個排隊號也是3號(next計數(shù))。無人辦理業(yè)務(wù)(鎖是釋放狀態(tài))。

linux中針對每一個spin lock會有兩個計數(shù)。分別是next和owner(初始值為0)。進程A申請鎖時,會判斷next和owner的值是否相等。如果相等就代表鎖可以申請成功,否則原地自旋。直到owner和next的值相等才會退出自旋。假設(shè)進程A申請鎖成功,然后會next加1。此時owner值為0,next值為1。進程B也申請鎖,保存next得值到局部變量tmp(tmp = 1)中。由于next和owner值不相等,因此原地自旋讀取owner的值,判斷owner和tmp是否相等,直到相等退出自旋狀態(tài)。當然next的值還是加1,變成2。進程A釋放鎖,此時會將owner的值加1,那么此時B進程的owner和tmp的值都是1,因此B進程獲得鎖。當B進程釋放鎖后,同樣會將owner的值加1。最后owner和next都等于2,代表沒有進程持有鎖。next就是一個記錄申請鎖的次數(shù),而owner是持有鎖進程的計數(shù)值。

實現(xiàn)

我們首先定義描述自旋鎖的結(jié)構(gòu)體arch_spinlock_t。

typedef struct {

union {

unsigned int slock;

struct __raw_tickets {

unsigned short owner;

unsigned short next;

} TIckets;

};

} arch_spinlock_t;

如上面的原理描述,我們需要兩個計數(shù),分別是owner和next。slock所占內(nèi)存區(qū)域覆蓋owner和next(據(jù)說C語言學好的都能看得懂)。下面實現(xiàn)申請鎖操作 arch_spin_lock。

staTIc inline void arch_spin_lock(arch_spinlock_t *lock)

{

arch_spinlock_t old_lock;

old_lock.slock = lock->slock; /* 1 */

lock->TIckets.next++; /* 2 */

while (old_lock.TIckets.next != old_lock.tickets.owner) { /* 3 */

wfe(); /* 4 */

old_lock.tickets.owner = lock->tickets.owner; /* 5 */

}

}

繼續(xù)上面的舉例。顧客從取號機器得到排隊號。

取號機器更新下個顧客將要拿到的排隊號。

看一下顯示屏,判斷是否輪到自己了。

wfe()函數(shù)是指ARM64架構(gòu)的WFE(wait for event)匯編指令。WFE是讓ARM核進入低功耗模式的指令。當進程拿不到鎖的時候,原地自旋不如cpu睡眠。節(jié)能。睡下去之后,什么時候醒來呢?就是等到持有鎖的進程釋放的時候,醒過來判斷是否可以持有鎖。如果不能獲得鎖,繼續(xù)睡眠即可。這里就相當于顧客先小憩一會,等到廣播下一位排隊者的時候,醒來看看是不是自己。

前臺已經(jīng)為上一個顧客辦理完成業(yè)務(wù),剩下排隊的顧客都要抬頭看一下顯示屏是不是輪到自己了。

釋放鎖的操作就非常簡單了。還記得上面銀行辦理業(yè)務(wù)的例子嗎?釋放鎖的操作僅僅是顯示屏上面的排隊號加1。我們僅僅需要將owner計數(shù)加1即可。arch_spin_unlock實現(xiàn)如下。

static inline void arch_spin_unlock(arch_spinlock_t *lock)

{

lock->tickets.owner++;

sev();

}

sev()函數(shù)是指ARM64架構(gòu)的SEV匯編指令。當進程無法獲取鎖的時候會使用WFE指令使CPU睡眠。現(xiàn)在釋放鎖了,自然要喚醒所有睡眠的CPU醒來檢查自己是不是可以獲取鎖。

信號量(semaphore)

信號量(semaphore)是進程間通信處理同步互斥的機制。是在多線程環(huán)境下使用的一種措施,它負責協(xié)調(diào)各個進程,以保證他們能夠正確、合理的使用公共資源。 它和spin lock最大的不同之處就是:無法獲取信號量的進程可以睡眠,因此會導致系統(tǒng)調(diào)度。

原理

信號量一般可以用來標記可用資源的個數(shù)。老規(guī)矩,還是舉個例子。假設(shè)圖書館有2本《C語言從入門到放棄》書籍。A同學想學C語言,于是發(fā)現(xiàn)這本書特別的好。于是就去學校的圖書館借書,A同學成功的從圖書館借走一本。這時,A同學室友B同學發(fā)現(xiàn)A同學竟然在偷偷的學習武功秘籍(C語言)。于是,B同學也去借一本。此時,圖書館已經(jīng)沒有書了。C同學也想借這本書,可能是這本書太火了。圖書館管理員告訴C同學,圖書館這本書都被借走了。如果有同學換回來,會第一時間通知你。于是,管理員就把C同學的信息登記先來,以備后續(xù)通知C同學來借書。所以,C同學只能悲傷的走了(如果是自旋鎖的原理的話,那么C同學將會端個小板凳坐在圖書館,一直要等到A同學或者B同學還書并借走)。

實現(xiàn)

為了記錄可用資源的數(shù)量,我們肯定需要一個count計數(shù),標記當前可用資源數(shù)量。當然還要一個可以像圖書管理員一樣的筆記本功能。用來記錄等待借書的同學。所以,一個雙向鏈表即可。因此只需要一個count計數(shù)和等待進程的鏈表頭即可。描述信號量的結(jié)構(gòu)體如下。

struct semaphore {

unsigned int count;

struct list_head wait_list;

};

在linux中,每個進程就相當于是每個借書的同學。通知一個同學,就相當于喚醒這個進程。因此,我們還需要一個結(jié)構(gòu)體記錄當前的進程信息(task_struct)。

struct semaphore_waiter {

struct list_head list;

struct task_struct *task;

};

struct semaphore_waiter的list成員是當進程無法獲取信號量的時候掛入semaphore的wait_list成員。task成員就是記錄后續(xù)被喚醒的進程信息。

一切準備就緒,現(xiàn)在就可以實現(xiàn)信號量的申請函數(shù)。

void down(struct semaphore *sem)

{

struct semaphore_waiter waiter;

if (sem->count > 0) {

sem->count--; /* 1 */

return;

}

waiter.task = current; /* 2 */

list_add_tail(&waiter.list, &sem->wait_list); /* 2 */

schedule(); /* 3 */

}

如果信號量標記的資源還有剩余,自然可以成功獲取信號量。只需要遞減可用資源計數(shù)。

既然無法獲取信號量,就需要將當前進程掛入信號量的等待隊列鏈表上。

schedule()主要是觸發(fā)任務(wù)調(diào)度的示意函數(shù),主動讓出CPU使用權(quán)。在讓出之前,需要將當前進程從運行隊列上移除。

釋放信號的實現(xiàn)也是比較簡單。實現(xiàn)如下。

void up(struct semaphore *sem)

{

struct semaphore_waiter waiter;

if (list_empty(&sem->wait_list)) {

sem->count++; /* 1 */

return;

}

waiter = list_first_entry(&sem->wait_list, struct semaphore_waiter, list);

list_del(&waiter->list); /* 2 */

wake_up_process(waiter->task); /* 2 */

}

如果等待鏈表沒有進程,那么自然只需要增加資源計數(shù)。

從等待進程鏈表頭取出第一個進程,并從鏈表上移除。然后就是喚醒該進程。

讀寫鎖(read-write lock)

不管是自旋鎖還是信號量在同一時間只能有一個進程進入臨界區(qū)。對于有些情況,我們是可以區(qū)分讀寫操作的。因此,我們希望對于讀操作的進程可以并發(fā)進行。對于寫操作只限于一個進程進入臨界區(qū)。而這種同步機制就是讀寫鎖。讀寫鎖一般具有以下幾種性質(zhì)。

同一時間有且僅有一個寫進程進入臨界區(qū)。

在沒有寫進程進入臨界區(qū)的時候,同時可以有多個讀進程進入臨界區(qū)。

讀進程和寫進程不可以同時進入臨界區(qū)。

讀寫鎖有兩種,一種是信號量類型,另一種是spin lock類型。下面以spin lock類型講解。

原理

老規(guī)矩,還是舉個例子理解讀寫鎖。我絞盡腦汁才想到一個比較貼切的例子。這個例子來源于生活。我發(fā)現(xiàn)公司一般都會有保潔阿姨打掃廁所。如果以男廁所為例的話,我覺得男士進入廁所就相當于讀者進入臨界區(qū)。因為可以有多個男士進廁所。而保潔阿姨進入男士廁所就相當于寫者進入臨界區(qū)。假設(shè)A男士發(fā)現(xiàn)保潔阿姨不在打掃廁所,就進入廁所。隨后B和C同時也進入廁所。然后保潔阿姨準備打掃廁所,發(fā)現(xiàn)有男士在廁所里面,因此只能在門口等待。ABC都離開了廁所。保潔阿姨迅速進入廁所打掃。然后D男士去上廁所,發(fā)現(xiàn)保潔阿姨在里面?;伊锪锏某鰜砹嗽陂T口等著。現(xiàn)在體會到了寫者(保潔阿姨)具有排他性,讀者(男士)可以并發(fā)進入臨界區(qū)了吧。

既然我們允許多個讀者進入臨界區(qū),因此我們需要一個計數(shù)統(tǒng)計讀者的個數(shù)。同時,由于寫者永遠只存在一個進入臨界區(qū),因此只需要一個bit標記是否有寫進程進入臨界區(qū)。所以,我們可以將兩個計數(shù)合二為一。只需要1個unsigned int類型即可。最高位(bit31)代表是否有寫者進入臨界區(qū),低31位(0~30bit)統(tǒng)計讀者個數(shù)。

+----+-------------------------------------------------+

| 31 | 30 0 |

+----+-------------------------------------------------+

| |

| +----> [0:30] Read Thread Counter

+-------------------------> [31] Write Thread Counter

實現(xiàn)

描述讀寫鎖只需要1個變量即可,因此我們可以定義讀寫鎖的結(jié)構(gòu)體如下。

typedef struct {

volatile unsigned int lock;

} arch_rwlock_t;

既然區(qū)分讀寫操作,因此肯定會有兩個申請鎖函數(shù),分別是讀和寫。首先,我們看一下read_lock操作的實現(xiàn)。

static inline void arch_read_lock(arch_rwlock_t *rw)

{

unsigned int tmp;

sevl(); /* 1 */

do {

wfe();

tmp = rw->lock;

tmp++; /* 2 */

} while(tmp & (1 << 31)); /* 3 */

rw->lock = tmp;

}

sevl()函數(shù)是ARM64架構(gòu)中SEVL匯編指令。SEVL和SEV的區(qū)別是,SEVL僅僅修改本地CPU的PE寄存器值,這樣下面的WFE指令第一次執(zhí)行的時候不會睡眠。

增加讀者計數(shù),最后會更新到rw->lock中。

更新rw->lock前提是沒有寫者,因此這里會判斷是否有寫者已經(jīng)進入臨界區(qū)(判斷方法是rw->lock變量bit31的值)。如果,有寫者已經(jīng)進入臨界區(qū),就在這里循環(huán),并WFE指令睡眠。類似上面介紹的spin lock實現(xiàn)。

當讀進程離開臨界區(qū)的時候會調(diào)用read_unlock釋放鎖。read_unlock實現(xiàn)如下。

static inline void arch_read_unlock(arch_rwlock_t *rw)

{

rw->lock--;

sev();

}

實現(xiàn)很簡單,和spin_unlock如出一轍。遞減讀者計數(shù),然后使用SEV指令喚醒所有的CPU,檢查等待狀態(tài)的進程是否可以獲取鎖。

讀操作看完了,我們看看寫操作是如何實現(xiàn)的。arch_write_lock實現(xiàn)如下。

static inline void arch_write_lock(arch_rwlock_t *rw)

{

unsigned int tmp;

sevl();

do {

wfe();

tmp = rw->lock;

} while(tmp); /* 1 */

rw->lock = 1 << 31; /* 2 */

}

由于寫者是排他的(讀者和寫者都不能有),因此這里只有rw->lock的值為0,當前的寫者才可以進入臨界區(qū)。

置位rw->lock的bit31,代表有寫者進入臨界區(qū)。

當寫進程離開臨界區(qū)的時候會調(diào)用write_unlock釋放鎖。write_unlock實現(xiàn)如下。

static inline void arch_write_unlock(arch_rwlock_t *rw)

{

rw->lock = 0; /* 1 */

sev(); /* 2 */

}

同樣由于寫者是排他的,因此只需要將rw->lock置0即可。代表沒有任何進程進入臨界區(qū)。畢竟是因為同一時間只能有一個寫者進入臨界區(qū),當這個寫者離開臨界區(qū)的時候,肯定是意味著現(xiàn)在沒有任何進程進入臨界區(qū)。

使用SEV指令喚醒所有的CPU,檢查等待狀態(tài)的進程是否可以獲取鎖。

以上的代碼實現(xiàn)其實會導致寫進程餓死現(xiàn)象。例如,A、B、C三個進程進入讀臨界區(qū),D進程嘗試獲得寫鎖,此時只能等待A、B、C三個進程退出臨界區(qū)。如果在退出之前又有F、G進程進入讀臨界區(qū),那么將出現(xiàn)D進程餓死現(xiàn)象。

互斥量(mutex)

前文提到的semaphore在初始化count計數(shù)的時候,可以分為計數(shù)信號量和互斥信號量(二值信號量)。mutex和初始化計數(shù)為1的二值信號量有很大的相似之處。他們都可以用做資源互斥。但是mutex卻有一個特殊的地方:只有持鎖者才能解鎖。但是,二值信號量卻可以在一個進程中獲取信號量,在另一個進程中釋放信號量。如果是應(yīng)用在嵌入式應(yīng)用的RTOS,針對mutex的實現(xiàn)還會考慮優(yōu)先級反轉(zhuǎn)問題。

原理

既然mutex是一種二值信號量,因此就不需要像semaphore那樣需要一個count計數(shù)。由于mutex具有“持鎖者才能解鎖”的特點,所以我們需要一個變量owner記錄持鎖進程。釋放鎖的時候必須是同一個進程才能釋放。當然也需要一個鏈表頭,主要用來便利睡眠等待的進程。原理和semaphore及其相似,因此在代碼上也有體現(xiàn)。

實現(xiàn)

mutex的實現(xiàn)代碼和linux中實現(xiàn)會有差異,但是依然可以為你呈現(xiàn)設(shè)計的原理。下面的設(shè)計代碼更像是部分RTOS中的代碼。mutex和semaphore一樣,我們需要兩個類似的結(jié)構(gòu)體分別描述mutex。

struct mutex_waiter {

struct list_head list;

struct task_struct *task;

};

struct mutex {

long owner;

struct list_head wait_list;

};

struct mutex_waiter的list成員是當進程無法獲取互斥量的時候掛入mutex的wait_list鏈表。

首先實現(xiàn)申請互斥量的函數(shù)。

void mutex_take(struct mutex *mutex)

{

struct mutex_waiter waiter;

if (!mutex->owner) {

mutex->owner = (long)current; /* 1 */

return;

}

waiter.task = current;

list_add_tail(&waiter.list, &mutex->wait_list); /* 2 */

schedule(); /* 2 */

}

當mutex->owner的值為0的時候,代表沒有任何進程持有鎖。因此可以直接申請成功。然后,記錄當前申請鎖進程的task_struct。

既然不能獲取互斥量,自然就需要睡眠等待,掛入等待鏈表。

互斥量的釋放代碼實現(xiàn)也同樣和semaphore有很多相似之處。不信,你看。

int mutex_release(struct mutex *mutex)

{

struct mutex_waiter *waiter;

if (mutex->owner != (long)current) /* 1 */

return -1;

if (list_empty(&mutex->wait_list)) {

mutex->owner = 0; /* 2 */

return 0;

}

waiter = list_first_entry(&mutex->wait_list, struct mutex_waiter, list);

list_del(&waiter->list);

mutex->owner = (long)waiter->task; /* 3 */

wake_up_process(waiter->task); /* 4 */

return 0;

}

mutex具有“持鎖者才能解鎖”的特點就是在這行代碼體現(xiàn)。

如果等待鏈表沒有進程,那么自然只需要將mutex->owner置0,代表沒有鎖是釋放狀態(tài)。

mutex->owner的值改成當前可以持鎖進程的task_struct。

從等待進程鏈表取出第一個進程,并從鏈表上移除。然后就是喚醒該進程。

這樣你對這個機制有所了解了嗎?

本站聲明: 本文章由作者或相關(guān)機構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內(nèi)容真實性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫毥谦F公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

加利福尼亞州圣克拉拉縣2024年8月30日 /美通社/ -- 數(shù)字化轉(zhuǎn)型技術(shù)解決方案公司Trianz今天宣布,該公司與Amazon Web Services (AWS)簽訂了...

關(guān)鍵字: AWS AN BSP 數(shù)字化

倫敦2024年8月29日 /美通社/ -- 英國汽車技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時1.5...

關(guān)鍵字: 汽車 人工智能 智能驅(qū)動 BSP

北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務(wù)能7×24不間斷運行,同時企業(yè)卻面臨越來越多業(yè)務(wù)中斷的風險,如企業(yè)系統(tǒng)復(fù)雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務(wù)連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報道,騰訊和網(wǎng)易近期正在縮減他們對日本游戲市場的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會開幕式在貴陽舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導體

8月28日消息,在2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會上,華為常務(wù)董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機 衛(wèi)星通信

要點: 有效應(yīng)對環(huán)境變化,經(jīng)營業(yè)績穩(wěn)中有升 落實提質(zhì)增效舉措,毛利潤率延續(xù)升勢 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務(wù)引領(lǐng)增長 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競爭力 堅持高質(zhì)量發(fā)展策略,塑強核心競爭優(yōu)勢...

關(guān)鍵字: 通信 BSP 電信運營商 數(shù)字經(jīng)濟

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺與中國電影電視技術(shù)學會聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會上宣布正式成立。 活動現(xiàn)場 NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會上,軟通動力信息技術(shù)(集團)股份有限公司(以下簡稱"軟通動力")與長三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉
關(guān)閉