linux kernel中的免鎖演算法

來源:互聯網
上載者:User

        在《LINUX裝置驅動程式》(第三版)有幾頁對免鎖演算法的實現進行了分析。對於作者的分析有兩點我想在這裡作更加細緻的說明。一是作者對迴圈緩衝的分析,當緩衝區滿時分析錯了;二是作者沒有對裡面的實現技巧作詳細的介紹。針對以上兩點,本文就用2.6.11(2.6.10和2.6.11是一樣的)的kfifo.h和kfifo.c代碼實現的免鎖演算法進行較為詳細的分析。        對於臨界區的訪問一般的做法是在訪問前加鎖,退出訪問時解鎖,在加鎖的過程中可能會有漫長的等待時間,也因此可能會影響到效率。如果在安全的情況下能夠進行免鎖訪問無疑是可以提高效率,使人振奮。當然做任何事情都受一定的環境、條件的影響和制約,免鎖演算法的實現也不例外。那麼在什麼情況下可以實現和使用免鎖演算法呢,其實代碼的作者Stelian在kfifo.c的注釋程式碼115--117已明確告訴了我們,如果只有一個讀者和一個寫者並發訪問臨界區的話就可以免鎖。以下是kfifo.h中聲明的迴圈緩衝資料結構: 

  29 struct kfifo {

 30         unsigned char *buffer; /* the buffer holding the data */ 31         unsigned int size;      /* the size of the allocated buffer */ 32         unsigned int in;        /* data is added at offset (in % size) */ 33         unsigned int out;       /* data is extracted from off. (out % size) */ 34         spinlock_t *lock;       /* protects concurrent modifications */ 35 };緩衝區buffer是以位元組為單位的迴圈緩衝區size是緩衝區的大小in是寫入資料時以 (in % size) 運算取得在buffer中的寫下標

out是讀取資料時以 (out % size) 運算取得在buffer中的讀下標

  

緩衝區buffer, size, in, out以一定的邏輯關係在代碼中組織起來, 下面結合圖A分析一下它們的特徵和邏輯關係:1.       緩衝區的特性是當緩衝區滿時,不允許向緩衝區寫資料,即緩衝區中的資料不會被覆蓋,緩衝區的資料為空白時不會讀取無用的資料。2.       在語義上in總是大於等於out,因此總會有( out
+ len ) <= in ,而len的取值範圍是0 <= len <= size-1。注意這裡是這語義上是in和out總是這樣的關係,其實在實際運算時如果不會超出無符號整形的表示範圍,in和out的關係無論是語義上還是邏輯上都是這種關係。3.       當 (in == out) 時,緩衝區中的資料為空白。當(
(in != out) && ((in % size) == (out % size)) ) 時,也即它們的讀寫下標相等,而in和out不等時,表示緩衝區滿。這種緩衝區滿的情況是資料佔滿了所有的緩衝空間,並非《LINUX裝置驅動程式》(第三版)描述的那樣有一個位元組浪費的情況。4.       在kfifo.c代碼的72--79行確保了size的值是2的n次方。5.       迴圈緩衝區的資料位元組數為(in - out),我們用BUF_DATA_BYTES來表示。相應地(size
- (in - out))則表示迴圈緩衝區還有多少位元組的空閑空間,我們就用BUF_FREE_SPACE來表示。6.       (size - (in % size))表示從寫下標開始到迴圈緩衝結束有多少位元組數,我們用WRETE_INDEX_TO_BUF_END_BYTES表示。相應地(size
- (out % size))表示從讀下標開始到迴圈緩衝結束有多少位元組數,我們用READ_INDEX_TO_BUF_END_BYTES。    

kfifo.c程式碼數不多,而我們要在這裡要分析的代碼則更少,主要是對緩衝區的讀寫介面get/put分析:

 

 ...............// 點代表省略的代碼72          /* 73           * round up to the next power of 2, since our 'let the indices 74           * wrap' tachnique works only in this case. 75           */             // 這裡的操作是確保size的值為2的n次方 76          if (size & (size - 1)) { 77                  BUG_ON(size > 0x80000000); 78                  size = roundup_pow_of_two(size); 79          }...............105 /*106   * __kfifo_put - puts some data into the FIFO, no locking version107   * @fifo: the fifo to be used.108   * @buffer: the data to be added.109   * @len: the length of the data to be added.110   *111   * This function copies at most 'len' bytes from the 'buffer' into112   * the FIFO depending on the free space, and returns the number of113   * bytes copied.114   *115   * Note that with only one concurrent reader and one concurrent116   * writer, you don't need extra locking to use these functions.117   */118 unsigned int __kfifo_put(struct kfifo *fifo,119                           unsigned char *buffer, unsigned int len)120 {121          unsigned int l;122      // 讀這段程式可結合圖B的分析      // min函數的第二個參數實為(fifo->size - (fifo->in - fifo->out)),      // 很顯然它就是上面提到的BUF_FREE_SPACE,BUF_FREE_SPACE和本// 函數__kfifo_put 參數len比較求出的最小值就是本次要寫入迴圈緩衝區// 的位元組個數,也即調用min函數後len為要寫入迴圈緩衝區的真正位元組個數// 注意:這裡的fifo->size可能是小於fifo->in,但fifo->size,// fifo->in, fifo->out三者都是無符號整形,運算中的邏輯不會出錯,這// 個技巧後面再解釋。123          len = min(len, fifo->size - fifo->in + fifo->out);124125          /* first put the data starting from fifo->in to buffer end */             /* 首先把資料寫到從寫下標開始到緩衝buffer結束 */      // (fifo->in & (fifo->size - 1))和(fifo->in % fifo->size)是等價的,// 這一技巧後面再解釋。既然它們是等價的,那麼// fifo->size - (fifo->in & (fifo->size - 1))就是// WRETE_INDEX_TO_BUF_END_BYTES。函數min目的就很顯然了,它求出來的// 是要寫的位元組數,而這位元組數的範圍是:從寫下標開始到迴圈緩衝結束。126          l = min(len, fifo->size - (fifo->in & (fifo->size - 1)));127          memcpy(fifo->buffer + (fifo->in & (fifo->size - 1)), buffer, l);128129          /* then put the rest (if any) at the beginning of the buffer */             /* 然後如果還有資料沒有寫入,就把剩下的位元組數len - l 從緩衝* 區buffer的開始處寫入剩餘資料*/130          memcpy(fifo->buffer, buffer + l, len - l);131      // 注意fifo->in只有加沒有減,當它達到它所能表達的最大範圍時會遵循      // 無符號整形的規則,這也是使用了無符號整形的技巧,也將在後面解釋。132          fifo->in += len;133134          return len;135 }136 EXPORT_SYMBOL(__kfifo_put);137138 /*139   * __kfifo_get - gets some data from the FIFO, no locking version140   * @fifo: the fifo to be used.141   * @buffer: where the data must be copied.142   * @len: the size of the destination buffer.143   *144   * This function copies at most 'len' bytes from the FIFO into the145   * 'buffer' and returns the number of copied bytes.146   *147   * Note that with only one concurrent reader and one concurrent148   * writer, you don't need extra locking to use these functions.149   */150 unsigned int __kfifo_get(struct kfifo *fifo,151                           unsigned char *buffer, unsigned int len)152 {153          unsigned int l;154       // 讀這段程式可結合圖B的分析        // 調用min函數後len為要讀取的真正位元組個數155          len = min(len, fifo->in - fifo->out);156157          /* first get the data from fifo->out until the end of the buffer */             /* 首先從讀下標開始讀取資料直到緩衝結束 */158          l = min(len, fifo->size - (fifo->out & (fifo->size - 1)));159          memcpy(buffer, fifo->buffer + (fifo->out & (fifo->size - 1)), l);160161          /* then get the rest (if any) from the beginning of the buffer */             /* 如果沒有讀完所需的位元組資料,就從緩衝的開始讀取剩下的位元組 */162          memcpy(buffer + l, fifo->buffer, len - l);163       // fifo->out也是只有加沒有減,跟fifo->in類似164          fifo->out += len;165166          return len;167 }

168 EXPORT_SYMBOL(__kfifo_get);

   現在我們結合圖C分析一個讀者和一個寫者並發訪問迴圈緩衝區的一個情景,看看免鎖的情形具體是怎樣的。首先假設size為8,out為0,in為5, A進程要寫10位元組資料到迴圈緩衝, B進程要從迴圈緩衝讀15位元組的資料。為了方便描述,我們把A進程簡稱為A, B進程簡稱為B。下面對兩進程A、B的7個時間段作假設性的分析: 1.       當A執行完 時間段1時,它把3位元組資料寫入了迴圈緩衝。但是A剛好執行到kfifo.c的第131行,而132行的寫下標更新代碼還沒執行就調度進程B。在這種情形下,資料雖然寫入到迴圈緩衝,但是寫下標還沒有更新,所以邏輯上緩衝區的位元組數還是in-out即5。2.       假如B在第2個時間段讀完了緩衝中所有的資料並更新了讀下標,那麼第2個時間段結束時,in和out的值都為5,表明緩衝中的資料為空白。3.       在第3個時間段還是調度B,雖然A已把資料寫入緩衝,但是還沒有更新寫下標,in和out的值都為5,緩衝中的資料看上去還是為空白,那麼B就可以直接調用發送器而放棄CUP。4.       第4時間段A把寫下標更新,發送器又調度B進程。5.       執行完第4時間段,in的值為8,out為5,有3位元組的資料在緩衝。第5時間段B把3位元組資料讀完後調用發送器放棄CPU。6.       第6時間段A把所有要寫的資料寫入了緩衝,第7時間段B讀出了它要讀的位元組數。 在這第3個時間段可看出,讀進程本質上已是在等待(寫進程等待情況也類似),但它不是加鎖解鎖的睡眠喚醒等待方式,它可以主動放棄CPU或輪循的方式。無論任何時候當讀進程被調度時,讀進程又可以隨時訪問緩衝區這個臨界區,而不受鎖的限制。由此可見免鎖的目的達到了。免鎖的目的達到了,但是kfifo.c又是如何保證資料的正確性呢?保證讀資料的正確性和保證寫資料的正確性情況類似,在這裡就只討論寫資料的情況。寫資料時正確性的保證有兩點:一是寫資料和寫下標更新的先後順序;二是求出正確的可寫位元組數。(一)在__kfifo_put介面中總體上是先寫資料後更新寫下標,這就確保了讀進程讀到的資料是正確了。如果反過來先更新寫下標後寫資料的話,而讀寫進程是並發訪問的,那麼寫進程有可能在更新下標後就進入睡眠等待狀態而把CPU交給了讀進程,這樣讀進程極有可能讀到不正確的資料。因為寫下標的更新表明了原來的修改範圍內的空閑空間變成了有資料的空間,也即變成了讀進程可以讀取資料的空間,而實際上寫進程還沒有把資料寫進去。       (二)在代碼的123行,通過和剩餘空閑空間的比較,求出了正確的可寫位元組數,確保寫資料時不會覆蓋掉緩衝中已存在的資料。   

最後把這一免鎖演算法用到的一些技巧作一些解釋說明:1.        二進位技巧:    如果一個數是2的n次方減1,那麼它的低n位都是1,其它位都為0。當一個數要被2的n次方整除,那麼它的低n位就必須為0,相反如果這個數的低n位不全為0那麼這個數就不能被2的n次方整除。而一個數的低n位就是對2的n次方資料求模運算的餘數。例如一個數要被8(2的3次方)整除,它的二進位低3位就得為0。如在16位機器中16和32的二進位分別是0000 0000 0001 0000和0000 0000 00010 0000,它們的低3 位全為0。而19和33在16位機器中的二進位分別是0000 0000 0001 0011和0000 0000 000100001,它們的低3位都不全為0,低3位是對8進行求模運算的餘數。能被2的n次方整除的這個特性還有別外的叫法:以某某(一般都是2的n次方)位元組對齊,以某某位元組為邊界。如以8位元組對齊,以頁面對齊等等。知道這一特性就可以用'&'操作判斷某一變數是否是以某某位元組對齊,例如判斷變數x是否以8位元組對齊
if(x&8), 也即x是否能被8整除或者說x是否是以8位元組為邊界。    那麼又如何解釋(fifo->in & (fifo->size - 1))和(fifo->in % fifo->size)是等價的呢?還是假設fifo->size為8,8是2 的3次方。(8-1)就是7,也就是低3位為1其它高位為0的位元據。7和一個整數進行&操作就是求一個數的低3位,也就是對整數8進行求模的餘數。2.        無符號整形技巧:    在32位機器中不帶正負號的整數的最大值是4294967295,最小值是0。那麼就有這樣的運算:((0-1) == 4294967295)、((0-2) == 4294967294),相反則((4294967295+1)== 0)、((4294967295+2)== 1)。    根據無符號的以上特性,可以解釋kfifo.c的程式碼132和164行fifo->in和fifo->out只有不斷地加而沒有減小的運算情況。當fifo->in和fifo->out的值超過了無符號整形能表示的最大值時,它們又可以從無符號整形最小值0開始。假如當fifo->in已超過了無符號整形最大值並且資料值現在已為2,而fifo->out值為4294967294,這時((fifo->out + 4)
== fifo->in),因此有上面提到的當len的取值範圍是0 <= len <= fifo->size-1,語義上總會有(fifo->out + len) <= fifo->in。    在kfifo.c代碼中都統一用了無符號整形,沒有和其它的有符號一起混用,這種處理效率會比多種類型值混用的運算效率會更高,因為不同類型的資料一起運算時要先進行資料類型的轉換。    另外要注意的是無符號整形和有符號整形一起運算時,首先會把有符號整形轉換為無符號整形,其運算結果是無符號的整形資料。運算時要特別注意無符號整形的範圍,更是要注意0值範圍。我曾兩次尋找過別人在這方面出現的BUG。BUG類似於這樣:if((x-y) > 0),x是無符號的整形,當x小於y時這個條件判斷就會有問題了。因為是無符號運算,x小於y時,算出來的結果超出了無符號最小值0的範圍,所以結果會變成遠遠大於0的值,致使最終判斷有誤。一個建議是不要在這種情況下用符號整形的運算。3.        迴圈緩衝技巧:    通過in、out技巧性操作實現,見程式的分析。
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.