cpu發生了上下文切換稱為經歷一個quiescent state,grace period就是所有cpu都經歷一次quiescent state所需要的等待的時間。

看到網文有說,linux rcu的寫操作更新時機,就是一次grace period,為什麼是這個時機?


這是個很難回答的問題, 要回答這個問題,需要理解一下為什麼需要RCU,如何實現RCU。那我就盡量回答一下吧,需要點耐心^_^。

提到多線程編程,大家都能想到加鎖,比如用戶態的pthread_mutex_lock/unlock和內核態的mutex_lock/spin_lock等加鎖機制。 這些機制的共同特點是,對任何加鎖的一方,都是公平對待的。

但在某些情況下,讀操作較多,而寫操作較少,如果使用公平方式的加鎖機制,對讀者不公平,造成整個系統的性能不是最優的,這裡讀寫鎖應運而生。

往前一步,在某些極端場景下,讀操作非常頻繁,而寫操作非常少,如果使用讀寫鎖,性能也較差。因為讀寫鎖儘管在宏觀上可並行,但在微觀上內部也要串列(加spin_lock 或原子變數),實際上也達不到高並行的效果。

這時候RCU出場了,它解決的問題是:讀操作非常頻繁,而寫操作卻異常地少,並且允許寫操作的修改,讀者延後一段時間才看到寫者的修改,也沒有邏輯問題。

一個典型的場景是路由器表,在大網路流量情況下,如果每個報文上來查路由表時,都加個spin_lock,那整個系統的吞吐會非常差,它必須是極端地偏向於讀者(查路由器過程),讓這個查表過程幾乎沒有任何開銷和串列化,才能提升系統的並行度。但偶爾還是會修改路由表,比如動態路由演算法學習到新的路由表,或者人工修改路由表,如何保障路由表數據結構的一致性,這就是RCU要乾的事情。

RCU是Read-Copy-Update的縮寫,它實際表示兩方面的意思:

1、對於讀者:你就認為沒有人會修改這個數據,你看到的數據永遠是一致的

2、對於寫者:如果你要修改數據,那麻煩請先拷貝出來到一個副本上(Read-Copy),然後在副本上修改,最新在適當時機,即不會影響讀者(努力創造讓讀者看到數據永遠是一致的假象下),將修改的數據更新回到讀者可見的數據處

如果你看過RCU的代碼,你會發現RCU總是在處理指針相關的數據結構,那是因為一般的數據結構,很難讓讀者看到永遠一致的數據。以下面的例子來說明一下吧:

假設系統要處理多個用戶,每個用戶的記錄如下:

struct user {
char name[32];
time_t birth;
int level;
// ...
};

然後有個用戶表,是一個數組

struct user user_table[128];

然後如何在該數組上實現RCU呢,答案是很難的。因為要讓讀者不感知寫者的存在,保障讀者看到的數據是永遠一致的,那是很難的。寫者要修改數據,肯定不能原地寫數據,先拷貝出來,然後在副本修改,那怎麼更新到原數據,讓讀者寫到的數據永遠一致呢,即要麼看到修改前的struct user要麼看到修改後的struct user,但是實現不了。因為數據中的每個struct user是超過8位元組的,沒有任何指令(或手段)讓這些內存空間,只有兩個狀態:要麼是讀寫copy過來之前的狀態,要麼是copy過來之後的狀態。讀者肯定會看到在copy過程之中的內存狀態,這樣無法保證是一致的了。

但是我們稍為將數據結構修改一下,變成指針類型的就可以了:

struct user *user_table[128];

為什麼指針的就可以了呢,是因為user_table[i]只是一個指針,8位元組的,將它修改成指向一個新的struct user結構,就可以實現個修改行為了,因為這些讀者要麼看到user_table[i]指向老的struct user結構,要麼指向新的struct user 結構,不會出現中間狀態,這樣就能完美解決一致性問題了。

具體讀者和寫者的偽代碼如下:

void rcu_read()
{
RCU_READ_START();
// 訪問table_user[i]指向的數據結構,可以按任何順序訪問
// 但是不能將訪table_user[i]指針傳遞給任何其它模塊,必須在
// RCU_READ_START和STOP內終止
RCU_READ_STOP();
}

void rcu_write()
{
struct user *new = malloc(struct user);
struct uer *old = user_table[i];

memcpy(new, old, sizeof(struct user));
// 根據實際代碼,對user_table[i]對象的修改,全部在new對象上進行修改
// 禁止原地修改

user_table[i] = new; // 修改完成後,可以立馬將對象指針更新到user_table[i]中
// 這樣會造成兩種情況,新訪問的reader會讀取到user_table[i]裡面是new對象的東西
// 而在早前一點訪問的reader讀取到user_table[i]裡面是old對象的東西
// 但無所謂,這是RCU預設的行為,這兩個對象都是數據一致的

asyn_free(old); // 這裡是非同步釋放old對象,但不能馬上釋放,因為此時還有讀者在使用它,
// 必須等待所有讀者使用完後,才能釋放它
// 所以:這裡的async_free有兩個意思,告訴rcu系統,你晚點時要幫我釋放old這個對象,
// 否則會造成內存泄漏, 另一個意思是:你必須在適應時釋,如果釋放得早一點,就跟
// 讀者打架了,造成邏輯錯誤,必須準確把握這個準確的時機。

}

RCU必須保證所有讀者沒有使用舊值(即old對象)之後,才能釋放old對象。那rcu怎麼知道reader有沒有訪問user_table[i]呢,什麼時候訪問完user_table[i]呢?

答案是 RCU_READ_START() 和 RCU_READ_STOP() 這兩個操作,它告訴操作系統正在訪問rcu讀者臨界區。

我們再返回寫者的代碼看看,如果在執行 asyn_free(old)代碼之後,才發起讀操作的reader(即調用rcu_read函數),那它看從user_table[i]裡面讀到的對象,肯定是new對象,是新值了,那釋放old對他沒有任何影響。 但是在async_free之前就發起讀操作的reader就不同了,不能保證它是否一定能看到新值,很可能是一直在使用舊值,RCU就只能假定這些reader看到的是舊值,必須等這些reader全部執行完RCU_READ_STOP()才能釋放old。

由於rcu偏向於讀者,開銷儘可能地低,不能要有串列化的操作,所以linux kernel裡面的RCU_READ_START() 裡面沒有使用加鎖,原子變數,或者全局變數,而是簡單地關搶佔。

而asyn_free的行為,就是問題所問及的。async_free過程原理也不難,它先在rcu登記一下,晚點要幫我釋放old對象。那什麼時候要釋放呀,就是當前所有讀者都退出了rcu臨界區,那退出臨界區的特點是什麼呢? 就是開搶佔了呀,即可以調度了,如果從這個時間點算起,如果每個cpu都經歷了一次調度,那就所有讀者(如果有的話)肯定都退出rcu臨界區了,可以安全地刪除old對象了。

在 Linux kernel實現中,RCU_READ_START 就是rcu_read_lock,而async_free就是 call_rcu。


樓上寫的太多了,因為rcu鎖是進去會幹掉內核搶佔的,讀鎖離開開搶佔,所以不存在沒有佔有cpu的獲得rcu讀的線程的,所以數是否發生了調度,簡單粗暴,發現所有cpu都調度過一次了說明沒有人讀了,這時候調用回調把數據刪了沒啥問題. RCU本身的設計我就不在這丟人了。去看Paul的書就行了。如果允許內核搶佔,這樣測度就沒有任何意義。這些測度結構本身和寫本神(比如鏈表next值換了)還是有屏障的,否則也玩不下去。

跟這個類似的是spin lock也會關調度,甚至關中斷的。感興趣可以研究下sleepable RCU,srcu,沒必要看文檔,直接看代碼就好了。你只有知道rcu read進去時做了啥,才能搞清楚什麼時候調寫掛的回調.


上下文切換時,要切換內存頁表,內存頁要換成下一個進程的。


Linux kernel裡面實現很多種rcu,最簡單的是srcu,直接擼代碼即可。其他幾個rcu實現的邏輯非常複雜,沒有幾個人講的清楚的。


大概是這麼個過程吧:

gp從當有線程寫時開始時計算,這時候寫的是copy的obj的副本,當寫那一刻所有的讀thread都結束後,gp結束,時鐘中斷中觸發rcu回調函數,完成真正對原始數據的寫。參考 淺析linux kernel rcu機制https://blog.csdn.net/think_ycx/article/details/81155672
推薦閱讀:
查看原文 >>
相关文章