linux驅動(續)

網絡通訊 --> IO多路複用之select、poll、epoll詳解

IO多路複用之select、poll、epoll詳解

     目前支持I/O多路複用的系統調用有  select,pselect,poll,epoll,I/O多路複用就是 經過一種機制,一個進程能夠監視多個描述符,一旦某個描述符就緒(通常是讀就緒或者寫就緒),可以通知程序進行相應的讀寫操做但select,pselect,poll,epoll本質上都是同步I/O,由於他們都須要在讀寫事件就緒後本身負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需本身負責進行讀寫,異步I/O的實現會負責把數據從內核拷貝到用戶空間。
     與多進程和多線程技術相比, I/O多路複用技術的最大優點是系統開銷小,系統沒必要建立進程/線程,也沒必要維護這些進程/線程,從而大大減少了系統的開銷。
 

1、使用場景

IO多路複用是指內核一旦發現進程指定的一個或者多個IO條件準備讀取,它就通知該進程。IO多路複用適用以下場合:
  1)當客戶處理多個描述符時(通常是交互式輸入和網絡套接口),必須使用I/O複用。
  2)當一個客戶同時處理多個套接口時,這種狀況是可能的,但不多出現。
  3)若是一個TCP服務器既要處理監聽套接口,又要處理已鏈接套接口,通常也要用到I/O複用。
  4)若是一個服務器即要處理TCP,又要處理UDP,通常要使用I/O複用。
  5)若是一個服務器要處理多個服務或多個協議,通常要使用I/O複用。
 

2、select、poll、epoll簡介

  epoll跟select都能提供多路I/O複用的解決方案。在如今的Linux內核裏有都可以支持, 其中epoll是Linux所特有,而select則應該是POSIX所規定,通常操做系統均有實現。

一、select

基本原理:select 函數監視的文件描述符分3類,分別是writefds、readfds、和exceptfds。調用後select函數會阻塞,直到有描述符就緒(有數據 可讀、可寫、或者有except),或者超時(timeout指定等待時間,若是當即返回設爲null便可),函數返回。當select函數返回後,能夠經過遍歷fdset,來找到就緒的描述符。
 
基本流程,如圖所示:
 
  select目前幾乎在全部的平臺上支持, 其良好跨平臺支持也是它的一個優勢select的一個缺點在於單個進程可以監視的文件描述符的數量存在最大限制,在Linux上通常爲1024,能夠經過修改宏定義甚至從新編譯內核的方式提高這一限制,可是這樣也會形成效率的下降。
 
select本質上是經過設置或者檢查存放fd標誌位的數據結構來進行下一步處理。這樣所帶來的缺點是:
一、select最大的缺陷就是單個進程所打開的FD是有必定限制的,它由FD_SETSIZE設置,默認值是1024
  通常來講這個數目和系統內存關係很大,具體數目能夠cat /proc/sys/fs/file-max察看。32位機默認是1024個。64位機默認是2048.
二、對socket進行掃描時是線性掃描,即採用輪詢的方法,效率較低。
  當套接字比較多的時候,每次select()都要經過遍歷FD_SETSIZE個Socket來完成調度,無論哪一個Socket是活躍的,都遍歷一遍。這會浪費不少CPU時間。若是能給套接字註冊某個回調函數,當他們活躍時,自動完成相關操做,那就避免了輪詢,這正是epoll與kqueue作的。
三、須要維護一個用來存放大量fd的數據結構,這樣會使得用戶空間和內核空間在傳遞該結構時複製開銷大。
 

二、poll

基本原理: poll本質上和select沒有區別,它將用戶傳入的數組拷貝到內核空間,而後查詢每一個fd對應的設備狀態,若是設備就緒則在設備等待隊列中加入一項並繼續遍歷,若是遍歷完全部fd後沒有發現就緒設備,則掛起當前進程,直到設備就緒或者主動超時,被喚醒後它又要再次遍歷fd。這個過程經歷了屢次無謂的遍歷。
 
它沒有最大鏈接數的限制,緣由是它是基於鏈表來存儲的,可是一樣有一個缺點:
1)大量的fd的數組被總體複製於用戶態和內核地址空間之間,而無論這樣的複製是否是有意義。
2)poll還有一個特色是「水平觸發」,若是報告了fd後,沒有被處理,那麼下次poll時會再次報告該fd。
 
注意:從上面看,select和poll都須要在返回後, 經過遍歷文件描述符來獲取已經就緒的socket。事實上, 同時鏈接的大量客戶端在一時刻可能只有不多的處於就緒狀態,所以隨着監視的描述符數量的增加,其效率也會線性降低。
 

三、epoll

  epoll是在2.6內核中提出的,是以前的select和poll的加強版本。相對於select和poll來講,epoll更加靈活,沒有描述符限制。 epoll使用一個文件描述符管理多個描述符,將用戶關係的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的copy只需一次
 
基本原理: epoll支持水平觸發和邊緣觸發,最大的特色在於邊緣觸發,它只告訴進程哪些fd剛剛變爲就緒態,而且只會通知一次。還有一個特色是, epoll使用「事件」的就緒通知方式,經過epoll_ctl註冊fd, 一旦該fd就緒,內核就會採用相似callback的回調機制來激活該fd,epoll_wait即可以收到通知。
 
epoll的優勢:
一、沒有最大併發鏈接的限制,能打開的FD的上限遠大於1024(1G的內存上能監聽約10萬個端口)。
二、效率提高,不是輪詢的方式,不會隨着FD數目的增長效率降低
  只有活躍可用的FD纔會調用callback函數; 即Epoll最大的優勢就在於它只管你「活躍」的鏈接,而跟鏈接總數無關,所以在實際的網絡環境中,Epoll的效率就會遠遠高於select和poll。
三、內存拷貝,利用mmap()文件映射內存加速與內核空間的消息傳遞;即epoll使用mmap減小複製開銷
 
epoll對文件描述符的操做有兩種模式: LT(level trigger)和ET(edge trigger)。LT模式是默認模式,LT模式與ET模式的區別以下:
LT模式:當epoll_wait檢測到描述符事件發生並將此事件通知應用程序, 應用程序能夠不當即處理該事件。下次調用epoll_wait時,會再次響應應用程序並通知此事件。
ET模式:當epoll_wait檢測到描述符事件發生並將此事件通知應用程序, 應用程序必須當即處理該事件。若是不處理,下次調用epoll_wait時,不會再次響應應用程序並通知此事件。
一、LT模式
  LT(level triggered)是缺省的工做方式,而且同時支持block和no-block socket。在這種作法中,內核告訴你一個文件描述符是否就緒了,而後你能夠對這個就緒的fd進行IO操做。 若是你不做任何操做,內核仍是會繼續通知你的
二、ET模式
  ET(edge-triggered)是高速工做方式,只支持no-block socket。在這種模式下,當描述符從未就緒變爲就緒時,內核經過epoll告訴你。而後它會假設你知道文件描述符已經就緒,而且不會再爲那個文件描述符發送更多的就緒通知,直到你作了某些操做致使那個文件描述符再也不爲就緒狀態了(好比,你在發送,接收或者接收請求,或者發送接收的數據少於必定量時致使了一個EWOULDBLOCK 錯誤)。 可是請注意,若是一直不對這個fd做IO操做(從而致使它再次變成未就緒),內核不會發送更多的通知(only once)
  ET模式在很大程度上減小了epoll事件被重複觸發的次數,所以效率要比LT模式高。epoll工做在ET模式的時候, 必須使用非阻塞套接口,以免因爲一個文件句柄的阻塞讀/阻塞寫操做把處理多個文件描述符的任務餓死。
三、在select/poll中, 進程只有在調用必定的方法後,內核纔對全部監視的文件描述符進行掃描,而epoll事先經過epoll_ctl()來註冊一個文件描述符, 一旦基於某個文件描述符就緒時,內核會採用相似callback的回調機制,迅速激活這個文件描述符,當進程調用epoll_wait()時便獲得通知。( 此處去掉了遍歷文件描述符,而是經過監聽回調的的機制。這正是epoll的魅力所在。)
注意:若是沒有大量的idle-connection或者dead-connection,epoll的效率並不會比select/poll高不少,可是當遇到大量的idle-connection,就會發現epoll的效率大大高於select/poll。
 

3、select、poll、epoll區別

一、支持一個進程所能打開的最大鏈接數

 

二、FD劇增後帶來的IO效率問題

 

三、消息傳遞方式

 
 
綜上,在選擇select,poll,epoll時要根據具體的使用場合以及這三種方式的自身特色:
一、表面上看epoll的性能最好, 可是在鏈接數少而且鏈接都十分活躍的狀況下,select和poll的性能可能比epoll好,畢竟epoll的通知機制須要不少函數回調。
二、select低效是由於每次它都須要輪詢。但低效也是相對的,視狀況而定,也可經過良好的設計改善。

 

 

 

詳述socket編程之select()和poll()函數

select()函數和poll()函數均是主要用來處理多路I/O複用的狀況。好比一個服務器既想等待輸入終端到來,又想等待若干個套接字有客戶請求到達,這時候就須要藉助select或者poll函數了。

(一)select()函數

原型以下:

css

1 int select(int fdsp1, fd_set *readfds, fd_set *writefds, fd_set *errorfds, const struct timeval *timeout);


各個參數含義以下:html

  • int fdsp1:最大描述符值 + 1
  • fd_set *readfds:對可讀感興趣的描述符集
  • fd_set *writefds:對可寫感興趣的描述符集
  • fd_set *errorfds:對出錯感興趣的描述符集
  • struct timeval *timeout:超時時間(注意:對於linux系統,此參數沒有const限制,每次select調用完畢timeout的值都被修改成剩餘時間,而unix系統則不會改變timeout值)

select函數會在發生如下狀況時返回:node

  1. readfds集合中有描述符可讀
  2. writefds集合中有描述符可寫
  3. errorfds集合中有描述符遇到錯誤條件
  4. 指定的超時時間timeout到了

當select返回時,描述符集合將被修改以指示哪些個描述符正處於可讀、可寫或有錯誤狀態。能夠用FD_ISSET宏對描述符進行測試以找到狀態變化的描述符。若是select由於超時而返回的話,全部的描述符集合都將被清空。
select函數返回狀態發生變化的描述符總數。返回0意味着超時。失敗則返回-1並設置errno。可能出現的錯誤有:EBADF(無效描述符)、EINTR(因終端而返回)、EINVAL(nfds或timeout取值錯誤)。
設置描述符集合一般用以下幾個宏定義:

linux

1 FD_ZERO(fd_set *fdset);                /* clear all bits in fdset           */
2 FD_SET(int fd, fd_set *fdset);         /* turn on the bit for fd in fd_set  */
3 FD_CLR(int fd, fd_set *fdset);         /* turn off the bit for fd in fd_set */
4 int FD_ISSET(int fd, fd_set *fdset);   /* is the bit for fd on in fdset?    */


如:

ios

1 fd_set rset;
2 FD_ZERO(&rset);                        /* initialize the set: all bits off  */
3 FD_SET(1, &rset);                      /* turn on bit for fd 1              */
4 FD_SET(4, &rset);                      /* turn on bit for fd 4              */
5 FD_SET(5, &rset);                      /* turn on bit for fd 5              */


當select返回的時候,rset位都將被置0,除了那些有變化的fd位。
當發生以下狀況時認爲是可讀的:程序員

  1. socket的receive buffer中的字節數大於socket的receive buffer的low-water mark屬性值。(low-water mark值相似於分水嶺,當receive buffer中的字節數小於low-water mark值的時候,認爲socket還不可讀,只有當receive buffer中的字節數達到必定量的時候才認爲socket可讀)
  2. 鏈接半關閉(讀關閉,即收到對端發來的FIN包)
  3. 發生變化的描述符是被動套接字,而鏈接的三路握手完成的數量大於0,即有新的TCP鏈接創建
  4. 描述符發生錯誤,若是調用read系統調用讀套接字的話會返回-1。

當發生以下狀況時認爲是可寫的:web

  1. socket的send buffer中的字節數大於socket的send buffer的low-water mark屬性值以及socket已經鏈接或者不須要鏈接(如UDP)。
  2. 寫半鏈接關閉,調用write函數將產生SIGPIPE
  3. 描述符發生錯誤,若是調用write系統調用寫套接字的話會返回-1。

注意:
select默認能處理的描述符數量是有上限的,爲FD_SETSIZE的大小。
對於timeout參數,若是置爲NULL,則表示wait forever;若timeout->tv_sec = timeout->tv_usec = 0,則表示do not wait at all;不然指定等待時間。
若是使用select處理多個套接字,那麼須要使用一個數組(也能夠是其餘結構)來記錄各個描述符的狀態。而使用poll則不須要,下面看poll函數。

(二)poll()函數

原型以下:

算法

1 int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);


各參數含義以下:sql

  • struct pollfd *fdarray:一個結構體,用來保存各個描述符的相關狀態。
  • unsigned long nfds:fdarray數組的大小,即裏面包含有效成員的數量。
  • int timeout:設定的超時時間。(以毫秒爲單位)

poll函數返回值及含義以下:編程

  • -1:有錯誤產生
  • 0:超時時間到,並且沒有描述符有狀態變化
  • >0:有狀態變化的描述符個數

着重講fdarray數組,由於這是它和select()函數主要的不一樣的地方:
pollfd的結構以下:

1 struct pollfd {
2    int fd;                  /* descriptor to check */
3    short events;      /* events of interest on fd */
4    short revents;     /* events that occured on fd */
5 };


其實poll()和select()函數要處理的問題是相同的,只不過是不一樣組織在幾乎相同時刻同時推出的,所以才同時保留了下來。select()函數把可讀描述符、可寫描述符、錯誤描述符分在了三個集合裏,這三個集合都是用bit位來標記一個描述符,一旦有若干個描述符狀態發生變化,那麼它將被置位,而其餘沒有發生變化的描述符的bit位將被clear,也就是說select()的readset、writeset、errorset是一個value-result類型,經過它們傳值,而也經過它們返回結果。這樣的一個壞處是每次從新select 的時候對集合必須從新賦值。而poll()函數則與select()採用的方式不一樣,它經過一個結構數組保存各個描述符的狀態,每一個結構體第一項fd表明描述符,第二項表明要監聽的事件,也就是感興趣的事件,而第三項表明poll()返回時描述符的返回狀態。合法狀態以下:

  • POLLIN:                有普通數據或者優先數據可讀
  • POLLRDNORM:    有普通數據可讀
  • POLLRDBAND:    有優先數據可讀
  • POLLPRI:              有緊急數據可讀
  • POLLOUT:            有普通數據可寫
  • POLLWRNORM:   有普通數據可寫
  • POLLWRBAND:    有緊急數據可寫
  • POLLERR:            有錯誤發生
  • POLLHUP:            有描述符掛起事件發生
  • POLLNVAL:          描述符非法


對於POLLIN | POLLPRI等價與select()的可讀事件;POLLOUT | POLLWRBAND等價與select()的可寫事件;POLLIN 等價與POLLRDNORM | POLLRDBAND,而POLLOUT等價於POLLWRBAND。若是你對一個描述符的可讀事件和可寫事件以及錯誤等事件均感興趣那麼你應該都進行相應的設置。
對於timeout的設置以下:

      • INFTIM:   wait forever
      • 0:            return immediately, do not block
      • >0:         wait specified number of milliseconds

 

 

Linux IO模式及 select、poll、epoll詳解

同步IO和異步IO,阻塞IO和非阻塞IO分別是什麼,到底有什麼區別?不一樣的人在不一樣的上下文下給出的答案是不一樣的。因此先限定一下本文的上下文。

本文討論的背景是Linux環境下的network IO。

一 概念說明

在進行解釋以前,首先要說明幾個概念:
- 用戶空間和內核空間
- 進程切換
- 進程的阻塞
- 文件描述符
- 緩存 I/O

用戶空間與內核空間

如今操做系統都是採用虛擬存儲器,那麼對32位操做系統而言,它的尋址空間(虛擬存儲空間)爲4G(2的32次方)。操做系統的核心是內核,獨立於普通的應用程序,能夠訪問受保護的內存空間,也有訪問底層硬件設備的全部權限。爲了保證用戶進程不能直接操做內核(kernel),保證內核的安全,操心繫統將虛擬空間劃分爲兩部分,一部分爲內核空間,一部分爲用戶空間。針對linux操做系統而言,將最高的1G字節(從虛擬地址0xC0000000到0xFFFFFFFF),供內核使用,稱爲內核空間,而將較低的3G字節(從虛擬地址0x00000000到0xBFFFFFFF),供各個進程使用,稱爲用戶空間。

進程切換

爲了控制進程的執行,內核必須有能力掛起正在CPU上運行的進程,並恢復之前掛起的某個進程的執行。這種行爲被稱爲進程切換。所以能夠說,任何進程都是在操做系統內核的支持下運行的,是與內核緊密相關的。

從一個進程的運行轉到另外一個進程上運行,這個過程當中通過下面這些變化:
1. 保存處理機上下文,包括程序計數器和其餘寄存器。
2. 更新PCB信息。
3. 把進程的PCB移入相應的隊列,如就緒、在某事件阻塞等隊列。
4. 選擇另外一個進程執行,並更新其PCB。
5. 更新內存管理的數據結構。
6. 恢復處理機上下文。

注:總而言之就是很耗資源,具體的能夠參考這篇文章:進程切換

進程的阻塞

正在執行的進程,因爲期待的某些事件未發生,如請求系統資源失敗、等待某種操做的完成、新數據還沒有到達或無新工做作等,則由系統自動執行阻塞原語(Block),使本身由運行狀態變爲阻塞狀態。可見,進程的阻塞是進程自身的一種主動行爲,也所以只有處於運行態的進程(得到CPU),纔可能將其轉爲阻塞狀態。當進程進入阻塞狀態,是不佔用CPU資源的

文件描述符fd

文件描述符(File descriptor)是計算機科學中的一個術語,是一個用於表述指向文件的引用的抽象化概念。

文件描述符在形式上是一個非負整數。實際上,它是一個索引值,指向內核爲每個進程所維護的該進程打開文件的記錄表。當程序打開一個現有文件或者建立一個新文件時,內核向進程返回一個文件描述符。在程序設計中,一些涉及底層的程序編寫每每會圍繞着文件描述符展開。可是文件描述符這一律念每每只適用於UNIX、Linux這樣的操做系統。

緩存 I/O

緩存 I/O 又被稱做標準 I/O,大多數文件系統的默認 I/O 操做都是緩存 I/O。在 Linux 的緩存 I/O 機制中,操做系統會將 I/O 的數據緩存在文件系統的頁緩存( page cache )中,也就是說,數據會先被拷貝到操做系統內核的緩衝區中,而後纔會從操做系統內核的緩衝區拷貝到應用程序的地址空間。

緩存 I/O 的缺點:
數據在傳輸過程當中須要在應用程序地址空間和內核進行屢次數據拷貝操做,這些數據拷貝操做所帶來的 CPU 以及內存開銷是很是大的。

二 IO模式

剛纔說了,對於一次IO訪問(以read舉例),數據會先被拷貝到操做系統內核的緩衝區中,而後纔會從操做系統內核的緩衝區拷貝到應用程序的地址空間。因此說,當一個read操做發生時,它會經歷兩個階段:
1. 等待數據準備 (Waiting for the data to be ready)
2. 將數據從內核拷貝到進程中 (Copying the data from the kernel to the process)

正式由於這兩個階段,linux系統產生了下面五種網絡模式的方案。
- 阻塞 I/O(blocking IO)
- 非阻塞 I/O(nonblocking IO)
- I/O 多路複用( IO multiplexing)
- 信號驅動 I/O( signal driven IO)
- 異步 I/O(asynchronous IO)

注:因爲signal driven IO在實際中並不經常使用,因此我這隻說起剩下的四種IO Model。

阻塞 I/O(blocking IO)

在linux中,默認狀況下全部的socket都是blocking,一個典型的讀操做流程大概是這樣:
clipboard.png

當用戶進程調用了recvfrom這個系統調用,kernel就開始了IO的第一個階段:準備數據(對於網絡IO來講,不少時候數據在一開始尚未到達。好比,尚未收到一個完整的UDP包。這個時候kernel就要等待足夠的數據到來)。這個過程須要等待,也就是說數據被拷貝到操做系統內核的緩衝區中是須要一個過程的。而在用戶進程這邊,整個進程會被阻塞(固然,是進程本身選擇的阻塞)。當kernel一直等到數據準備好了,它就會將數據從kernel中拷貝到用戶內存,而後kernel返回結果,用戶進程才解除block的狀態,從新運行起來。

因此,blocking IO的特色就是在IO執行的兩個階段都被block了。

非阻塞 I/O(nonblocking IO)

linux下,能夠經過設置socket使其變爲non-blocking。當對一個non-blocking socket執行讀操做時,流程是這個樣子:
clipboard.png

當用戶進程發出read操做時,若是kernel中的數據尚未準備好,那麼它並不會block用戶進程,而是馬上返回一個error。從用戶進程角度講 ,它發起一個read操做後,並不須要等待,而是立刻就獲得了一個結果。用戶進程判斷結果是一個error時,它就知道數據尚未準備好,因而它能夠再次發送read操做。一旦kernel中的數據準備好了,而且又再次收到了用戶進程的system call,那麼它立刻就將數據拷貝到了用戶內存,而後返回。

因此,nonblocking IO的特色是用戶進程須要不斷的主動詢問kernel數據好了沒有。

I/O 多路複用( IO multiplexing)

IO multiplexing就是咱們說的select,poll,epoll,有些地方也稱這種IO方式爲event driven IO。select/epoll的好處就在於單個process就能夠同時處理多個網絡鏈接的IO。它的基本原理就是select,poll,epoll這個function會不斷的輪詢所負責的全部socket,當某個socket有數據到達了,就通知用戶進程。

clipboard.png

當用戶進程調用了select,那麼整個進程會被block,而同時,kernel會「監視」全部select負責的socket,當任何一個socket中的數據準備好了,select就會返回。這個時候用戶進程再調用read操做,將數據從kernel拷貝到用戶進程。

因此,I/O 多路複用的特色是經過一種機制一個進程能同時等待多個文件描述符,而這些文件描述符(套接字描述符)其中的任意一個進入讀就緒狀態,select()函數就能夠返回。

這個圖和blocking IO的圖其實並無太大的不一樣,事實上,還更差一些。由於這裏須要使用兩個system call (select 和 recvfrom),而blocking IO只調用了一個system call (recvfrom)。可是,用select的優點在於它能夠同時處理多個connection。

因此,若是處理的鏈接數不是很高的話,使用select/epoll的web server不必定比使用multi-threading + blocking IO的web server性能更好,可能延遲還更大。select/epoll的優點並非對於單個鏈接能處理得更快,而是在於能處理更多的鏈接。)

在IO multiplexing Model中,實際中,對於每個socket,通常都設置成爲non-blocking,可是,如上圖所示,整個用戶的process實際上是一直被block的。只不過process是被select這個函數block,而不是被socket IO給block。

異步 I/O(asynchronous IO)

inux下的asynchronous IO其實用得不多。先看一下它的流程:
clipboard.png

用戶進程發起read操做以後,馬上就能夠開始去作其它的事。而另外一方面,從kernel的角度,當它受到一個asynchronous read以後,首先它會馬上返回,因此不會對用戶進程產生任何block。而後,kernel會等待數據準備完成,而後將數據拷貝到用戶內存,當這一切都完成以後,kernel會給用戶進程發送一個signal,告訴它read操做完成了。

總結

blocking和non-blocking的區別

調用blocking IO會一直block住對應的進程直到操做完成,而non-blocking IO在kernel還準備數據的狀況下會馬上返回。

synchronous IO和asynchronous IO的區別

在說明synchronous IO和asynchronous IO的區別以前,須要先給出二者的定義。POSIX的定義是這樣子的:
- A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
- An asynchronous I/O operation does not cause the requesting process to be blocked;

二者的區別就在於synchronous IO作」IO operation」的時候會將process阻塞。按照這個定義,以前所述的blocking IO,non-blocking IO,IO multiplexing都屬於synchronous IO。

有人會說,non-blocking IO並無被block啊。這裏有個很是「狡猾」的地方,定義中所指的」IO operation」是指真實的IO操做,就是例子中的recvfrom這個system call。non-blocking IO在執行recvfrom這個system call的時候,若是kernel的數據沒有準備好,這時候不會block進程。可是,當kernel中數據準備好的時候,recvfrom會將數據從kernel拷貝到用戶內存中,這個時候進程是被block了,在這段時間內,進程是被block的。

而asynchronous IO則不同,當進程發起IO 操做以後,就直接返回不再理睬了,直到kernel發送一個信號,告訴進程說IO完成。在這整個過程當中,進程徹底沒有被block。

各個IO Model的比較如圖所示:
clipboard.png

經過上面的圖片,能夠發現non-blocking IO和asynchronous IO的區別仍是很明顯的。在non-blocking IO中,雖然進程大部分時間都不會被block,可是它仍然要求進程去主動的check,而且當數據準備完成之後,也須要進程主動的再次調用recvfrom來將數據拷貝到用戶內存。而asynchronous IO則徹底不一樣。它就像是用戶進程將整個IO操做交給了他人(kernel)完成,而後他人作完後發信號通知。在此期間,用戶進程不須要去檢查IO操做的狀態,也不須要主動的去拷貝數據。

三 I/O 多路複用之select、poll、epoll詳解

select,poll,epoll都是IO多路複用的機制。I/O多路複用就是經過一種機制,一個進程能夠監視多個描述符,一旦某個描述符就緒(通常是讀就緒或者寫就緒),可以通知程序進行相應的讀寫操做。但select,poll,epoll本質上都是同步I/O,由於他們都須要在讀寫事件就緒後本身負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需本身負責進行讀寫,異步I/O的實現會負責把數據從內核拷貝到用戶空間。(這裏囉嗦下)

select

int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); 

select 函數監視的文件描述符分3類,分別是writefds、readfds、和exceptfds。調用後select函數會阻塞,直到有描述副就緒(有數據 可讀、可寫、或者有except),或者超時(timeout指定等待時間,若是當即返回設爲null便可),函數返回。當select函數返回後,能夠 經過遍歷fdset,來找到就緒的描述符。

select目前幾乎在全部的平臺上支持,其良好跨平臺支持也是它的一個優勢。select的一 個缺點在於單個進程可以監視的文件描述符的數量存在最大限制,在Linux上通常爲1024,能夠經過修改宏定義甚至從新編譯內核的方式提高這一限制,但 是這樣也會形成效率的下降。

poll

int poll (struct pollfd *fds, unsigned int nfds, int timeout); 

不一樣與select使用三個位圖來表示三個fdset的方式,poll使用一個 pollfd的指針實現。

struct pollfd { int fd; /* file descriptor */ short events; /* requested events to watch */ short revents; /* returned events witnessed */ }; 

pollfd結構包含了要監視的event和發生的event,再也不使用select「參數-值」傳遞的方式。同時,pollfd並無最大數量限制(可是數量過大後性能也是會降低)。 和select函數同樣,poll返回後,須要輪詢pollfd來獲取就緒的描述符。

從上面看,select和poll都須要在返回後,經過遍歷文件描述符來獲取已經就緒的socket。事實上,同時鏈接的大量客戶端在一時刻可能只有不多的處於就緒狀態,所以隨着監視的描述符數量的增加,其效率也會線性降低。

epoll

epoll是在2.6內核中提出的,是以前的select和poll的加強版本。相對於select和poll來講,epoll更加靈活,沒有描述符限制。epoll使用一個文件描述符管理多個描述符,將用戶關係的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的copy只需一次。

一 epoll操做過程

epoll操做過程須要三個接口,分別以下:

int epoll_create(int size);//建立一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); 

1. int epoll_create(int size);
建立一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大,這個參數不一樣於select()中的第一個參數,給出最大監聽的fd+1的值,參數size並非限制了epoll所能監聽的描述符最大個數,只是對內核初始分配內部數據結構的一個建議
當建立好epoll句柄後,它就會佔用一個fd值,在linux下若是查看/proc/進程id/fd/,是可以看到這個fd的,因此在使用完epoll後,必須調用close()關閉,不然可能致使fd被耗盡。

2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函數是對指定描述符fd執行op操做。
- epfd:是epoll_create()的返回值。
- op:表示op操做,用三個宏來表示:添加EPOLL_CTL_ADD,刪除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分別添加、刪除和修改對fd的監聽事件。
- fd:是須要監聽的fd(文件描述符)
- epoll_event:是告訴內核須要監聽什麼事,struct epoll_event結構以下:

struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; //events能夠是如下幾個宏的集合: EPOLLIN :表示對應的文件描述符能夠讀(包括對端SOCKET正常關閉); EPOLLOUT:表示對應的文件描述符能夠寫; EPOLLPRI:表示對應的文件描述符有緊急的數據可讀(這裏應該表示有帶外數據到來); EPOLLERR:表示對應的文件描述符發生錯誤; EPOLLHUP:表示對應的文件描述符被掛斷; EPOLLET: 將EPOLL設爲邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來講的。 EPOLLONESHOT:只監聽一次事件,當監聽完此次事件以後,若是還須要繼續監聽這個socket的話,須要再次把這個socket加入到EPOLL隊列裏 

3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待epfd上的io事件,最多返回maxevents個事件。
參數events用來從內核獲得事件的集合,maxevents告以內核這個events有多大,這個maxevents的值不能大於建立epoll_create()時的size,參數timeout是超時時間(毫秒,0會當即返回,-1將不肯定,也有說法說是永久阻塞)。該函數返回須要處理的事件數目,如返回0表示已超時。

二 工做模式

 epoll對文件描述符的操做有兩種模式:LT(level trigger)ET(edge trigger)。LT模式是默認模式,LT模式與ET模式的區別以下:
  LT模式:當epoll_wait檢測到描述符事件發生並將此事件通知應用程序,應用程序能夠不當即處理該事件。下次調用epoll_wait時,會再次響應應用程序並通知此事件。
  ET模式:當epoll_wait檢測到描述符事件發生並將此事件通知應用程序,應用程序必須當即處理該事件。若是不處理,下次調用epoll_wait時,不會再次響應應用程序並通知此事件。

1. LT模式

LT(level triggered)是缺省的工做方式,而且同時支持block和no-block socket.在這種作法中,內核告訴你一個文件描述符是否就緒了,而後你能夠對這個就緒的fd進行IO操做。若是你不做任何操做,內核仍是會繼續通知你的。

2. ET模式

ET(edge-triggered)是高速工做方式,只支持no-block socket。在這種模式下,當描述符從未就緒變爲就緒時,內核經過epoll告訴你。而後它會假設你知道文件描述符已經就緒,而且不會再爲那個文件描述符發送更多的就緒通知,直到你作了某些操做致使那個文件描述符再也不爲就緒狀態了(好比,你在發送,接收或者接收請求,或者發送接收的數據少於必定量時致使了一個EWOULDBLOCK 錯誤)。可是請注意,若是一直不對這個fd做IO操做(從而致使它再次變成未就緒),內核不會發送更多的通知(only once)

ET模式在很大程度上減小了epoll事件被重複觸發的次數,所以效率要比LT模式高。epoll工做在ET模式的時候,必須使用非阻塞套接口,以免因爲一個文件句柄的阻塞讀/阻塞寫操做把處理多個文件描述符的任務餓死。

3. 總結

假若有這樣一個例子:
1. 咱們已經把一個用來從管道中讀取數據的文件句柄(RFD)添加到epoll描述符
2. 這個時候從管道的另外一端被寫入了2KB的數據
3. 調用epoll_wait(2),而且它會返回RFD,說明它已經準備好讀取操做
4. 而後咱們讀取了1KB的數據
5. 調用epoll_wait(2)......

LT模式:
若是是LT模式,那麼在第5步調用epoll_wait(2)以後,仍然能受到通知。

ET模式:
若是咱們在第1步將RFD添加到epoll描述符的時候使用了EPOLLET標誌,那麼在第5步調用epoll_wait(2)以後將有可能會掛起,由於剩餘的數據還存在於文件的輸入緩衝區內,並且數據發出端還在等待一個針對已經發出數據的反饋信息。只有在監視的文件句柄上發生了某個事件的時候 ET 工做模式纔會彙報事件。所以在第5步的時候,調用者可能會放棄等待仍在存在於文件輸入緩衝區內的剩餘數據。

當使用epoll的ET模型來工做時,當產生了一個EPOLLIN事件後,
讀數據的時候須要考慮的是當recv()返回的大小若是等於請求的大小,那麼頗有多是緩衝區還有數據未讀完,也意味着該次事件尚未處理完,因此還須要再次讀取:

while(rs){ buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0); if(buflen < 0){ // 因爲是非阻塞的模式,因此當errno爲EAGAIN時,表示當前緩衝區已無數據可讀 // 在這裏就看成是該次事件已處理處. if(errno == EAGAIN){ break; } else{ return; } } else if(buflen == 0){ // 這裏表示對端的socket已正常關閉. } if(buflen == sizeof(buf){ rs = 1; // 須要再次讀取 } else{ rs = 0; } } 

Linux中的EAGAIN含義

Linux環境下開發常常會碰到不少錯誤(設置errno),其中EAGAIN是其中比較常見的一個錯誤(好比用在非阻塞操做中)。
從字面上來看,是提示再試一次。這個錯誤常常出如今當應用程序進行一些非阻塞(non-blocking)操做(對文件或socket)的時候。

例如,以 O_NONBLOCK的標誌打開文件/socket/FIFO,若是你連續作read操做而沒有數據可讀。此時程序不會阻塞起來等待數據準備就緒返回,read函數會返回一個錯誤EAGAIN,提示你的應用程序如今沒有數據可讀請稍後再試。
又例如,當一個系統調用(好比fork)由於沒有足夠的資源(好比虛擬內存)而執行失敗,返回EAGAIN提示其再調用一次(也許下次就能成功)。

三 代碼演示

下面是一段不完整的代碼且格式不對,意在表述上面的過程,去掉了一些模板代碼。

#define IPADDRESS "127.0.0.1" #define PORT 8787 #define MAXSIZE 1024 #define LISTENQ 5 #define FDSIZE 1000 #define EPOLLEVENTS 100 listenfd = socket_bind(IPADDRESS,PORT); struct epoll_event events[EPOLLEVENTS]; //建立一個描述符 epollfd = epoll_create(FDSIZE); //添加監聽描述符事件 add_event(epollfd,listenfd,EPOLLIN); //循環等待 for ( ; ; ){ //該函數返回已經準備好的描述符事件數目 ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1); //處理接收到的鏈接 handle_events(epollfd,events,ret,listenfd,buf); } //事件處理函數 static void handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf) { int i; int fd; //進行遍歷;這裏只要遍歷已經準備好的io事件。num並非當初epoll_create時的FDSIZE。 for (i = 0;i < num;i++) { fd = events[i].data.fd; //根據描述符的類型和事件類型進行處理 if ((fd == listenfd) &&(events[i].events & EPOLLIN)) handle_accpet(epollfd,listenfd); else if (events[i].events & EPOLLIN) do_read(epollfd,fd,buf); else if (events[i].events & EPOLLOUT) do_write(epollfd,fd,buf); } } //添加事件 static void add_event(int epollfd,int fd,int state){ struct epoll_event ev; ev.events = state; ev.data.fd = fd; epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev); } //處理接收到的鏈接 static void handle_accpet(int epollfd,int listenfd){ int clifd; struct sockaddr_in cliaddr; socklen_t cliaddrlen; clifd = accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddrlen); if (clifd == -1) perror("accpet error:"); else { printf("accept a new client: %s:%d\n",inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port); //添加一個客戶描述符和事件 add_event(epollfd,clifd,EPOLLIN); } } //讀處理 static void do_read(int epollfd,int fd,char *buf){ int nread; nread = read(fd,buf,MAXSIZE); if (nread == -1) { perror("read error:"); close(fd); //記住close fd delete_event(epollfd,fd,EPOLLIN); //刪除監聽 } else if (nread == 0) { fprintf(stderr,"client close.\n"); close(fd); //記住close fd delete_event(epollfd,fd,EPOLLIN); //刪除監聽 } else { printf("read message is : %s",buf); //修改描述符對應的事件,由讀改成寫 modify_event(epollfd,fd,EPOLLOUT); } } //寫處理 static void do_write(int epollfd,int fd,char *buf) { int nwrite; nwrite = write(fd,buf,strlen(buf)); if (nwrite == -1){ perror("write error:"); close(fd); //記住close fd delete_event(epollfd,fd,EPOLLOUT); //刪除監聽 }else{ modify_event(epollfd,fd,EPOLLIN); } memset(buf,0,MAXSIZE); } //刪除事件 static void delete_event(int epollfd,int fd,int state) { struct epoll_event ev; ev.events = state; ev.data.fd = fd; epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&ev); } //修改事件 static void modify_event(int epollfd,int fd,int state){ struct epoll_event ev; ev.events = state; ev.data.fd = fd; epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev); } //注:另一端我就省了 

四 epoll總結

在 select/poll中,進程只有在調用必定的方法後,內核纔對全部監視的文件描述符進行掃描,而epoll事先經過epoll_ctl()來註冊一 個文件描述符,一旦基於某個文件描述符就緒時,內核會採用相似callback的回調機制,迅速激活這個文件描述符,當進程調用epoll_wait() 時便獲得通知。(此處去掉了遍歷文件描述符,而是經過監聽回調的的機制。這正是epoll的魅力所在。)

epoll的優勢主要是一下幾個方面:
1. 監視的描述符數量不受限制,它所支持的FD上限是最大能夠打開文件的數目,這個數字通常遠大於2048,舉個例子,在1GB內存的機器上大約是10萬左 右,具體數目能夠cat /proc/sys/fs/file-max察看,通常來講這個數目和系統內存關係很大。select的最大缺點就是進程打開的fd是有數量限制的。這對 於鏈接數量比較大的服務器來講根本不能知足。雖然也能夠選擇多進程的解決方案( Apache就是這樣實現的),不過雖然linux上面建立進程的代價比較小,但仍舊是不可忽視的,加上進程間數據同步遠比不上線程間同步的高效,因此也不是一種完美的方案。

  1. IO的效率不會隨着監視fd的數量的增加而降低。epoll不一樣於select和poll輪詢的方式,而是經過每一個fd定義的回調函數來實現的。只有就緒的fd纔會執行回調函數。

若是沒有大量的idle -connection或者dead-connection,epoll的效率並不會比select/poll高不少,可是當遇到大量的idle- connection,就會發現epoll的效率大大高於select/poll。

參考

用戶空間與內核空間,進程上下文與中斷上下文[總結]
進程切換
維基百科-文件描述符
Linux 中直接 I/O 機制的介紹
IO - 同步,異步,阻塞,非阻塞 (亡羊補牢篇)
Linux中select poll和epoll的區別
IO多路複用之select總結
IO多路複用之poll總結
IO多路複用之epoll總結

 

 

 

 

select、poll、epoll之間的區別總結[整理]

  select,poll,epoll都是IO多路複用的機制。I/O多路複用就經過一種機制,能夠監視多個描述符,一旦某個描述符就緒(通常是讀就緒或者寫就緒),可以通知程序進行相應的讀寫操做。但select,poll,epoll本質上都是同步I/O,由於他們都須要在讀寫事件就緒後本身負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需本身負責進行讀寫,異步I/O的實現會負責把數據從內核拷貝到用戶空間。關於這三種IO多路複用的用法,前面三篇總結寫的很清楚,並用服務器回射echo程序進行了測試。鏈接以下所示:

select:http://www.cnblogs.com/Anker/archive/2013/08/14/3258674.html

poll:http://www.cnblogs.com/Anker/archive/2013/08/15/3261006.html

epoll:http://www.cnblogs.com/Anker/archive/2013/08/17/3263780.html

  今天對這三種IO多路複用進行對比,參考網上和書上面的資料,整理以下:

一、select實現

select的調用過程以下所示:

(1)使用copy_from_user從用戶空間拷貝fd_set到內核空間

(2)註冊回調函數__pollwait

(3)遍歷全部fd,調用其對應的poll方法(對於socket,這個poll方法是sock_poll,sock_poll根據狀況會調用到tcp_poll,udp_poll或者datagram_poll)

(4)以tcp_poll爲例,其核心實現就是__pollwait,也就是上面註冊的回調函數。

(5)__pollwait的主要工做就是把current(當前進程)掛到設備的等待隊列中,不一樣的設備有不一樣的等待隊列,對於tcp_poll來講,其等待隊列是sk->sk_sleep(注意把進程掛到等待隊列中並不表明進程已經睡眠了)。在設備收到一條消息(網絡設備)或填寫完文件數據(磁盤設備)後,會喚醒設備等待隊列上睡眠的進程,這時current便被喚醒了。

(6)poll方法返回時會返回一個描述讀寫操做是否就緒的mask掩碼,根據這個mask掩碼給fd_set賦值。

(7)若是遍歷完全部的fd,尚未返回一個可讀寫的mask掩碼,則會調用schedule_timeout是調用select的進程(也就是current)進入睡眠。當設備驅動發生自身資源可讀寫後,會喚醒其等待隊列上睡眠的進程。若是超過必定的超時時間(schedule_timeout指定),仍是沒人喚醒,則調用select的進程會從新被喚醒得到CPU,進而從新遍歷fd,判斷有沒有就緒的fd。

(8)把fd_set從內核空間拷貝到用戶空間。

總結:

select的幾大缺點:

(1)每次調用select,都須要把fd集合從用戶態拷貝到內核態,這個開銷在fd不少時會很大

(2)同時每次調用select都須要在內核遍歷傳遞進來的全部fd,這個開銷在fd不少時也很大

(3)select支持的文件描述符數量過小了,默認是1024

2 poll實現

  poll的實現和select很是類似,只是描述fd集合的方式不一樣,poll使用pollfd結構而不是select的fd_set結構,其餘的都差很少。

關於select和poll的實現分析,能夠參考下面幾篇博文:

http://blog.csdn.net/lizhiguo0532/article/details/6568964#comments

http://blog.csdn.net/lizhiguo0532/article/details/6568968

http://blog.csdn.net/lizhiguo0532/article/details/6568969

http://www.ibm.com/developerworks/cn/linux/l-cn-edntwk/index.html?ca=drs-

http://linux.chinaunix.net/techdoc/net/2009/05/03/1109887.shtml

三、epoll

  epoll既然是對select和poll的改進,就應該能避免上述的三個缺點。那epoll都是怎麼解決的呢?在此以前,咱們先看一下epoll和select和poll的調用接口上的不一樣,select和poll都只提供了一個函數——select或者poll函數。而epoll提供了三個函數,epoll_create,epoll_ctl和epoll_wait,epoll_create是建立一個epoll句柄;epoll_ctl是註冊要監聽的事件類型;epoll_wait則是等待事件的產生。

  對於第一個缺點,epoll的解決方案在epoll_ctl函數中。每次註冊新的事件到epoll句柄中時(在epoll_ctl中指定EPOLL_CTL_ADD),會把全部的fd拷貝進內核,而不是在epoll_wait的時候重複拷貝。epoll保證了每一個fd在整個過程當中只會拷貝一次。

  對於第二個缺點,epoll的解決方案不像select或poll同樣每次都把current輪流加入fd對應的設備等待隊列中,而只在epoll_ctl時把current掛一遍(這一遍必不可少)併爲每一個fd指定一個回調函數,當設備就緒,喚醒等待隊列上的等待者時,就會調用這個回調函數,而這個回調函數會把就緒的fd加入一個就緒鏈表)。epoll_wait的工做實際上就是在這個就緒鏈表中查看有沒有就緒的fd(利用schedule_timeout()實現睡一會,判斷一會的效果,和select實現中的第7步是相似的)。

  對於第三個缺點,epoll沒有這個限制,它所支持的FD上限是最大能夠打開文件的數目,這個數字通常遠大於2048,舉個例子,在1GB內存的機器上大約是10萬左右,具體數目能夠cat /proc/sys/fs/file-max察看,通常來講這個數目和系統內存關係很大。

總結:

(1)select,poll實現須要本身不斷輪詢全部fd集合,直到設備就緒,期間可能要睡眠和喚醒屢次交替。而epoll其實也須要調用epoll_wait不斷輪詢就緒鏈表,期間也可能屢次睡眠和喚醒交替,可是它是設備就緒時,調用回調函數,把就緒fd放入就緒鏈表中,並喚醒在epoll_wait中進入睡眠的進程。雖然都要睡眠和交替,可是select和poll在「醒着」的時候要遍歷整個fd集合,而epoll在「醒着」的時候只要判斷一下就緒鏈表是否爲空就好了,這節省了大量的CPU時間。這就是回調機制帶來的性能提高。

(2)select,poll每次調用都要把fd集合從用戶態往內核態拷貝一次,而且要把current往設備等待隊列中掛一次,而epoll只要一次拷貝,並且把current往等待隊列上掛也只掛一次(在epoll_wait的開始,注意這裏的等待隊列並非設備等待隊列,只是一個epoll內部定義的等待隊列)。這也能節省很多的開銷。

參考資料:

http://www.cnblogs.com/apprentice89/archive/2013/05/09/3070051.html

http://www.linuxidc.com/Linux/2012-05/59873p3.htm

http://xingyunbaijunwei.blog.163.com/blog/static/76538067201241685556302/

http://blog.csdn.net/kkxgx/article/details/7717125

https://banu.com/blog/2/how-to-use-epoll-a-complete-example-in-c/epoll-example.c

 

 

 

 

Linux I/O複用中select poll epoll模型的介紹及其優缺點的比較

關於I/O多路複用:

I/O多路複用(又被稱爲「事件驅動」),首先要理解的是,操做系統爲你提供了一個功能,當你的某個socket可讀或者可寫的時候,它能夠給你一個通知。這樣當配合非阻塞的socket使用時,只有當系統通知我哪一個描述符可讀了,我纔去執行read操做,能夠保證每次read都能讀到有效數據而不作純返回-1和EAGAIN的無用功。寫操做相似。操做系統的這個功能經過select/poll/epoll之類的系統調用來實現,這些函數均可以同時監視多個描述符的讀寫就緒情況,這樣,**多個描述符的I/O操做都能在一個線程內併發交替地順序完成,這就叫I/O多路複用,這裏的「複用」指的是複用同一個線程。

1、I/O複用之select

一、介紹:
select系統調用的目的是:在一段指定時間內,監聽用戶感興趣的文件描述符上的可讀、可寫和異常事件。poll和select應該被歸類爲這樣的系統調用,它們能夠阻塞地同時探測一組支持非阻塞的IO設備,直至某一個設備觸發了事件或者超過了指定的等待時間——也就是說它們的職責不是作IO,而是幫助調用者尋找當前就緒的設備。
下面是select的原理圖:
這裏寫圖片描述

二、select系統調用API以下:

#include <sys/time.h> #include <sys/types.h> #include <unistd.h> int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

 

fd_set結構體是文件描述符集,該結構體其實是一個整型數組,數組中的每一個元素的每一位標記一個文件描述符。fd_set能容納的文件描述符數量由FD_SETSIZE指定,通常狀況下,FD_SETSIZE等於1024,這就限制了select能同時處理的文件描述符的總量。

三、下面介紹一下各個參數的含義:
1)nfds參數指定被監聽的文件描述符的總數。一般被設置爲select監聽的全部文件描述符中最大值加1;
2)readfds、writefds、exceptfds分別指向可讀、可寫和異常等事件對應的文件描述符集合。這三個參數都是傳入傳出型參數,指的是在調用select以前,用戶把關心的可讀、可寫、或異常的文件描述符經過FD_SET(下面介紹)函數分別添加進readfds、writefds、exceptfds文件描述符集,select將對這些文件描述符集中的文件描述符進行監聽,若是有就緒文件描述符,select會重置readfds、writefds、exceptfds文件描述符集來通知應用程序哪些文件描述符就緒。這個特性將致使select函數返回後,再次調用select以前,必須重置咱們關心的文件描述符,也就是三個文件描述符集已經不是咱們以前傳入 的了。
3)timeout參數用來指定select函數的超時時間(下面講select返回值時還會談及)。

struct timeval { long tv_sec; //秒數 long tv_usec; //微秒數 };

 

四、下面幾個函數(宏實現)用來操縱文件描述符集:

void FD_SET(int fd, fd_set *set); //在set中設置文件描述符fd void FD_CLR(int fd, fd_set *set); //清除set中的fd位 int FD_ISSET(int fd, fd_set *set); //判斷set中是否設置了文件描述符fd void FD_ZERO(fd_set *set); //清空set中的全部位(在使用文件描述符集前,應該先清空一下) //(注意FD_CLR和FD_ZERO的區別,一個是清除某一位,一個是清除全部位)

 

五、select的返回狀況:
1)若是指定timeout爲NULL,select會永遠等待下去,直到有一個文件描述符就緒,select返回;
2)若是timeout的指定時間爲0,select根本不等待,當即返回;
3)若是指定一段固定時間,則在這一段時間內,若是有指定的文件描述符就緒,select函數返回,若是超過指定時間,select一樣返回。
4)返回值狀況:
a)超時時間內,若是文件描述符就緒,select返回就緒的文件描述符總數(包括可讀、可寫和異常),若是沒有文件描述符就緒,select返回0;
b)select調用失敗時,返回 -1並設置errno,若是收到信號,select返回 -1並設置errno爲EINTR。

六、文件描述符的就緒條件:
在網絡編程中,
1)下列狀況下socket可讀:
a) socket內核接收緩衝區的字節數大於或等於其低水位標記SO_RCVLOWAT;
b) socket通訊的對方關閉鏈接,此時該socket可讀,可是一旦讀該socket,會當即返回0(能夠用這個方法判斷client端是否斷開鏈接);
c) 監聽socket上有新的鏈接請求;
d) socket上有未處理的錯誤。
2)下列狀況下socket可寫:
a) socket內核發送緩衝區的可用字節數大於或等於其低水位標記SO_SNDLOWAT;
b) socket的讀端關閉,此時該socket可寫,一旦對該socket進行操做,該進程會收到SIGPIPE信號;
c) socket使用connect鏈接成功以後;
d) socket上有未處理的錯誤。

2、I/O複用之poll

一、poll系統調用的原理與原型和select基本相似,也是在指定時間內輪詢必定數量的文件描述符,以測試其中是否有就緒者。

二、poll系統調用API以下:

#include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout);

 

三、下面介紹一下各個參數的含義:
1)第一個參數是指向一個結構數組的第一個元素的指針,每一個元素都是一個pollfd結構,用於指定測試某個給定描述符的條件。

struct pollfd { int fd; //指定要監聽的文件描述符 short events; //指定監聽fd上的什麼事件 short revents; //fd上事件就緒後,用於保存實際發生的時間 };

 

待監聽的事件由events成員指定,函數在相應的revents成員中返回該描述符的狀態(每一個文件描述符都有兩個事件,一個是傳入型的events,一個是傳出型的revents,從而避免使用傳入傳出型參數,注意與select的區別),從而告知應用程序fd上實際發生了哪些事件。events和revents均可以是多個事件的按位或。
2)第二個參數是要監聽的文件描述符的個數,也就是數組fds的元素個數;
3)第三個參數意義與select相同。

四、poll的事件類型:
這裏寫圖片描述
在使用POLLRDHUP時,要在代碼開始處定義_GNU_SOURCE

五、poll的返回狀況:
與select相同。

3、I/O複用之epoll

一、介紹:
epoll 與select和poll在使用和實現上有很大區別。首先,epoll使用一組函數來完成,而不是單獨的一個函數;其次,epoll把用戶關心的文件描述符上的事件放在內核裏的一個事件表中,無須向select和poll那樣每次調用都要重複傳入文件描述符集合事件集。

二、建立一個文件描述符,指定內核中的事件表:

#include<sys/epoll.h> int epoll_create(int size); //調用成功返回一個文件描述符,失敗返回-1並設置errno。

 

size參數並不起做用,只是給內核一個提示,告訴它事件表須要多大。該函數返回的文件描述符指定要訪問的內核事件表,是其餘全部epoll系統調用的句柄。

三、操做內核事件表:

#include<sys/epoll.h> int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); //調用成功返回0,調用失敗返回-1並設置errno。

 

epfd是epoll_create返回的文件句柄,標識事件表,op指定操做類型。操做類型有如下3種:

a)EPOLL_CTL_ADD, 往事件表中註冊fd上的事件; b)EPOLL_CTL_MOD, 修改fd上註冊的事件; c)EPOLL_CTL_DEL, 刪除fd上註冊的事件。

 

event參數指定事件,epoll_event的定義以下:

struct epoll_event { __int32_t events; //epoll事件 epoll_data_t data; //用戶數據 }; typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; }epoll_data;

 

在使用epoll_ctl時,是把fd添加、修改到內核事件表中,或從內核事件表中刪除fd的事件。若是是添加事件到事件表中,能夠往data中的fd上添加事件events,或者不用data中的fd,而把fd放到用戶數據ptr所指的內存中(由於epoll_data是一個聯合體,只能使用其中一個數據),再設置events。

三、epoll_wait函數
epoll系統調用的最關鍵的一個函數epoll_wait,它在一段時間內等待一個組文件描述符上的事件。

#include<sys/epoll.h> int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); //函數調用成功返回就緒文件描述符個數,失敗返回-1並設置errno。

 

timeout參數和select與poll相同,指定一個超時時間;maxevents指定最多監聽多少個事件;events是一個傳出型參數,epoll_wait函數若是檢測到事件就緒,就將全部就緒的事件從內核事件表(epfd所指的文件)中複製到events指定的數組中。這個數組用來輸出epoll_wait檢測到的就緒事件,而不像select與poll那樣,這也是epoll與前者最大的區別,下文在比較三者之間的區別時還會說到。

4、三組I/O複用函數的比較

相同點:
1)三者都須要在fd上註冊用戶關心的事件;
2)三者都要一個timeout參數指定超時時間;
不一樣點:
1)select:
a)select指定三個文件描述符集,分別是可讀、可寫和異常事件,因此不能更加細緻地區分全部可能發生的事件;
b)select若是檢測到就緒事件,會在原來的文件描述符上改動,以告知應用程序,文件描述符上發生了什麼時間,因此再次調用select時,必須先重置文件描述符
c)select採用對全部註冊的文件描述符集輪詢的方式,會返回整個用戶註冊的事件集合,因此應用程序索引就緒文件的時間複雜度爲O(n);
d)select容許監聽的最大文件描述符個數一般有限制,通常是1024,若是大於1024,select的性能會急劇降低;
e)只能工做在LT模式。

2)poll:
a)poll把文件描述符和事件綁定,事件不但能夠單獨指定,並且能夠是多個事件的按位或,這樣更加細化了事件的註冊,並且poll單獨採用一個元素用來保存就緒返回時的結果,這樣在下次調用poll時,就不用重置以前註冊的事件;
b)poll採用對全部註冊的文件描述符集輪詢的方式,會返回整個用戶註冊的事件集合,因此應用程序索引就緒文件的時間複雜度爲O(n)。
c)poll用nfds參數指定最多監聽多少個文件描述符和事件,這個數能達到系統容許打開的最大文件描述符數目,即65535。
d)只能工做在LT模式。

3)epoll:
a)epoll把用戶註冊的文件描述符和事件放到內核當中的事件表中,提供了一個獨立的系統調用epoll_ctl來管理用戶的事件,並且epoll採用回調的方式,一旦有註冊的文件描述符就緒,講觸發回調函數,該回調函數將就緒的文件描述符和事件拷貝到用戶空間events所管理的內存,這樣應用程序索引就緒文件的時間複雜度達到O(1)。
b)epoll_wait使用maxevents來制定最多監聽多少個文件描述符和事件,這個數能達到系統容許打開的最大文件描述符數目,即65535;
c)不只能工做在LT模式,並且還支持ET高效模式(即EPOLLONESHOT事件,讀者能夠本身查一下這個事件類型,對於epoll的線程安全有很好的幫助)。

select/poll/epoll總結:
這裏寫圖片描述

 

 

 

 

深度理解select、poll和epoll

在linux 沒有實現epoll事件驅動機制以前,咱們通常選擇用select或者poll等IO多路複用的方法來實現併發服務程序。在大數據、高併發、集羣等一些名詞唱得火熱之年代,select和poll的用武之地愈來愈有限,風頭已經被epoll佔盡。

本文便來介紹epoll的實現機制,並附帶講解一下select和poll。經過對比其不一樣的實現機制,真正理解爲什麼epoll能實現高併發。

select()和poll() IO多路複用模型

select的缺點:

  1. 單個進程可以監視的文件描述符的數量存在最大限制,一般是1024,固然能夠更改數量,但因爲select採用輪詢的方式掃描文件描述符,文件描述符數量越多,性能越差;(在linux內核頭文件中,有這樣的定義:#define __FD_SETSIZE    1024)
  2. 內核 / 用戶空間內存拷貝問題,select須要複製大量的句柄數據結構,產生巨大的開銷;
  3. select返回的是含有整個句柄的數組,應用程序須要遍歷整個數組才能發現哪些句柄發生了事件;
  4. select的觸發方式是水平觸發,應用程序若是沒有完成對一個已經就緒的文件描述符進行IO操做,那麼以後每次select調用仍是會將這些文件描述符通知進程。

相比select模型,poll使用鏈表保存文件描述符,所以沒有了監視文件數量的限制,但其餘三個缺點依然存在。

拿select模型爲例,假設咱們的服務器須要支持100萬的併發鏈接,則在__FD_SETSIZE 爲1024的狀況下,則咱們至少須要開闢1k個進程才能實現100萬的併發鏈接。除了進程間上下文切換的時間消耗外,從內核/用戶空間大量的無腦內存拷貝、數組輪詢等,是系統難以承受的。所以,基於select模型的服務器程序,要達到10萬級別的併發訪問,是一個很難完成的任務。

所以,該epoll上場了。

epoll IO多路複用模型實現機制

因爲epoll的實現機制與select/poll機制徹底不一樣,上面所說的 select的缺點在epoll上不復存在。

設想一下以下場景:有100萬個客戶端同時與一個服務器進程保持着TCP鏈接。而每一時刻,一般只有幾百上千個TCP鏈接是活躍的(事實上大部分場景都是這種狀況)。如何實現這樣的高併發?

在select/poll時代,服務器進程每次都把這100萬個鏈接告訴操做系統(從用戶態複製句柄數據結構到內核態),讓操做系統內核去查詢這些套接字上是否有事件發生,輪詢完後,再將句柄數據複製到用戶態,讓服務器應用程序輪詢處理已發生的網絡事件,這一過程資源消耗較大,所以,select/poll通常只能處理幾千的併發鏈接。

epoll的設計和實現與select徹底不一樣。epoll經過在Linux內核中申請一個簡易的文件系統(文件系統通常用什麼數據結構實現?B+樹)。把原先的select/poll調用分紅了3個部分:

1)調用epoll_create()創建一個epoll對象(在epoll文件系統中爲這個句柄對象分配資源)

2)調用epoll_ctl向epoll對象中添加這100萬個鏈接的套接字

3)調用epoll_wait收集發生的事件的鏈接

如此一來,要實現上面說是的場景,只須要在進程啓動時創建一個epoll對象,而後在須要的時候向這個epoll對象中添加或者刪除鏈接。同時,epoll_wait的效率也很是高,由於調用epoll_wait時,並無一股腦的向操做系統複製這100萬個鏈接的句柄數據,內核也不須要去遍歷所有的鏈接。

下面來看看Linux內核具體的epoll機制實現思路。

當某一進程調用epoll_create方法時,Linux內核會建立一個eventpoll結構體,這個結構體中有兩個成員與epoll的使用方式密切相關。eventpoll結構體以下所示:

  1. struct eventpoll{  
  2.     ....  
  3.     /*紅黑樹的根節點,這顆樹中存儲着全部添加到epoll中的須要監控的事件*/  
  4.     struct rb_root  rbr;  
  5.     /*雙鏈表中則存放着將要經過epoll_wait返回給用戶的知足條件的事件*/  
  6.     struct list_head rdlist;  
  7.     ....  
  8. };  

每個epoll對象都有一個獨立的eventpoll結構體,用於存放經過epoll_ctl方法向epoll對象中添加進來的事件。這些事件都會掛載在紅黑樹中,如此,重複添加的事件就能夠經過紅黑樹而高效的識別出來(紅黑樹的插入時間效率是lgn,其中n爲樹的高度)。

而全部添加到epoll中的事件都會與設備(網卡)驅動程序創建回調關係,也就是說,當相應的事件發生時會調用這個回調方法。這個回調方法在內核中叫ep_poll_callback,它會將發生的事件添加到rdlist雙鏈表中。

在epoll中,對於每個事件,都會創建一個epitem結構體,以下所示:

  1. struct epitem{  
  2.     struct rb_node  rbn;//紅黑樹節點  
  3.     struct list_head    rdllink;//雙向鏈表節點  
  4.     struct epoll_filefd  ffd;  //事件句柄信息  
  5.     struct eventpoll *ep;    //指向其所屬的eventpoll對象  
  6.     struct epoll_event event; //期待發生的事件類型  
  7. }  

當調用epoll_wait檢查是否有事件發生時,只須要檢查eventpoll對象中的rdlist雙鏈表中是否有epitem元素便可。若是rdlist不爲空,則把發生的事件複製到用戶態,同時將事件數量返回給用戶。

epoll.jpg

epoll數據結構示意圖

從上面的講解可知:經過紅黑樹和雙鏈表數據結構,並結合回調機制,造就了epoll的高效。

OK,講解完了Epoll的機理,咱們便能很容易掌握epoll的用法了。一句話描述就是:三步曲。

第一步:epoll_create()系統調用。此調用返回一個句柄,以後全部的使用都依靠這個句柄來標識。

第二步:epoll_ctl()系統調用。經過此調用向epoll對象中添加、刪除、修改感興趣的事件,返回0標識成功,返回-1表示失敗。

第三部:epoll_wait()系統調用。經過此調用收集收集在epoll監控中已經發生的事件。

最後,附上一個epoll編程實例。(做者爲sparkliang)

  1. //     
  2. // a simple echo server using epoll in linux    
  3. //     
  4. // 2009-11-05    
  5. // 2013-03-22:修改了幾個問題,1是/n格式問題,2是去掉了原代碼不當心加上的ET模式;  
  6. // 原本只是簡單的示意程序,決定仍是加上 recv/send時的buffer偏移  
  7. // by sparkling    
  8. //     
  9. #include <sys/socket.h>    
  10. #include <sys/epoll.h>    
  11. #include <netinet/in.h>    
  12. #include <arpa/inet.h>    
  13. #include <fcntl.h>    
  14. #include <unistd.h>    
  15. #include <stdio.h>    
  16. #include <errno.h>    
  17. #include <iostream>    
  18. using namespace std;    
  19. #define MAX_EVENTS 500    
  20. struct myevent_s    
  21. {    
  22.     int fd;    
  23.     void (*call_back)(int fd, int events, void *arg);    
  24.     int events;    
  25.     void *arg;    
  26.     int status; // 1: in epoll wait list, 0 not in    
  27.     char buff[128]; // recv data buffer    
  28.     int len, s_offset;    
  29.     long last_active; // last active time    
  30. };    
  31. // set event    
  32. void EventSet(myevent_s *ev, int fd, void (*call_back)(int, int, void*), void *arg)    
  33. {    
  34.     ev->fd = fd;    
  35.     ev->call_back = call_back;    
  36.     ev->events = 0;    
  37.     ev->arg = arg;    
  38.     ev->status = 0;  
  39.     bzero(ev->buff, sizeof(ev->buff));  
  40.     ev->s_offset = 0;    
  41.     ev->len = 0;  
  42.     ev->last_active = time(NULL);    
  43. }    
  44. // add/mod an event to epoll    
  45. void EventAdd(int epollFd, int events, myevent_s *ev)    
  46. {    
  47.     struct epoll_event epv = {0, {0}};    
  48.     int op;    
  49.     epv.data.ptr = ev;    
  50.     epv.events = ev->events = events;    
  51.     if(ev->status == 1){    
  52.         op = EPOLL_CTL_MOD;    
  53.     }    
  54.     else{    
  55.         op = EPOLL_CTL_ADD;    
  56.         ev->status = 1;    
  57.     }    
  58.     if(epoll_ctl(epollFd, op, ev->fd, &epv) < 0)    
  59.         printf("Event Add failed[fd=%d], evnets[%d]\n", ev->fd, events);    
  60.     else    
  61.         printf("Event Add OK[fd=%d], op=%d, evnets[%0X]\n", ev->fd, op, events);    
  62. }    
  63. // delete an event from epoll    
  64. void EventDel(int epollFd, myevent_s *ev)    
  65. {    
  66.     struct epoll_event epv = {0, {0}};    
  67.     if(ev->status != 1) return;    
  68.     epv.data.ptr = ev;    
  69.     ev->status = 0;  
  70.     epoll_ctl(epollFd, EPOLL_CTL_DEL, ev->fd, &epv);    
  71. }    
  72. int g_epollFd;    
  73. myevent_s g_Events[MAX_EVENTS+1]; // g_Events[MAX_EVENTS] is used by listen fd    
  74. void RecvData(int fd, int events, void *arg);    
  75. void SendData(int fd, int events, void *arg);    
  76. // accept new connections from clients    
  77. void AcceptConn(int fd, int events, void *arg)    
  78. {    
  79.     struct sockaddr_in sin;    
  80.     socklen_t len = sizeof(struct sockaddr_in);    
  81.     int nfd, i;    
  82.     // accept    
  83.     if((nfd = accept(fd, (struct sockaddr*)&sin, &len)) == -1)    
  84.     {    
  85.         if(errno != EAGAIN && errno != EINTR)    
  86.         {    
  87.         }  
  88.         printf("%s: accept, %d", __func__, errno);    
  89.         return;    
  90.     }    
  91.     do    
  92.     {    
  93.         for(i = 0; i < MAX_EVENTS; i++)    
  94.         {    
  95.             if(g_Events[i].status == 0)    
  96.             {    
  97.                 break;    
  98.             }    
  99.         }    
  100.         if(i == MAX_EVENTS)    
  101.         {    
  102.             printf("%s:max connection limit[%d].", __func__, MAX_EVENTS);    
  103.             break;    
  104.         }    
  105.         // set nonblocking  
  106.         int iret = 0;  
  107.         if((iret = fcntl(nfd, F_SETFL, O_NONBLOCK)) < 0)  
  108.         {  
  109.             printf("%s: fcntl nonblocking failed:%d", __func__, iret);  
  110.             break;  
  111.         }  
  112.         // add a read event for receive data    
  113.         EventSet(&g_Events[i], nfd, RecvData, &g_Events[i]);    
  114.         EventAdd(g_epollFd, EPOLLIN, &g_Events[i]);    
  115.     }while(0);    
  116.     printf("new conn[%s:%d][time:%d], pos[%d]\n", inet_ntoa(sin.sin_addr),  
  117.             ntohs(sin.sin_port), g_Events[i].last_active, i);    
  118. }    
  119. // receive data    
  120. void RecvData(int fd, int events, void *arg)    
  121. {    
  122.     struct myevent_s *ev = (struct myevent_s*)arg;    
  123.     int len;    
  124.     // receive data  
  125.     len = recv(fd, ev->buff+ev->len, sizeof(ev->buff)-1-ev->len, 0);      
  126.     EventDel(g_epollFd, ev);  
  127.     if(len > 0)  
  128.     {  
  129.         ev->len += len;  
  130.         ev->buff[len] = '\0';    
  131.         printf("C[%d]:%s\n", fd, ev->buff);    
  132.         // change to send event    
  133.         EventSet(ev, fd, SendData, ev);    
  134.         EventAdd(g_epollFd, EPOLLOUT, ev);    
  135.     }    
  136.     else if(len == 0)    
  137.     {    
  138.         close(ev->fd);    
  139.         printf("[fd=%d] pos[%d], closed gracefully.\n", fd, ev-g_Events);    
  140.     }    
  141.     else    
  142.     {    
  143.         close(ev->fd);    
  144.         printf("recv[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno));    
  145.     }    
  146. }    
  147. // send data    
  148. void SendData(int fd, int events, void *arg)    
  149. {    
  150.     struct myevent_s *ev = (struct myevent_s*)arg;    
  151.     int len;    
  152.     // send data    
  153.     len = send(fd, ev->buff + ev->s_offset, ev->len - ev->s_offset, 0);  
  154.     if(len > 0)    
  155.     {  
  156.         printf("send[fd=%d], [%d<->%d]%s\n", fd, len, ev->len, ev->buff);  
  157.         ev->s_offset += len;  
  158.         if(ev->s_offset == ev->len)  
  159.         {  
  160.             // change to receive event  
  161.             EventDel(g_epollFd, ev);    
  162.             EventSet(ev, fd, RecvData, ev);    
  163.             EventAdd(g_epollFd, EPOLLIN, ev);    
  164.         }  
  165.     }    
  166.     else    
  167.     {    
  168.         close(ev->fd);    
  169.         EventDel(g_epollFd, ev);    
  170.         printf("send[fd=%d] error[%d]\n", fd, errno);    
  171.     }    
  172. }    
  173. void InitListenSocket(int epollFd, short port)    
  174. {    
  175.     int listenFd = socket(AF_INET, SOCK_STREAM, 0);    
  176.     fcntl(listenFd, F_SETFL, O_NONBLOCK); // set non-blocking    
  177.     printf("server listen fd=%d\n", listenFd);    
  178.     EventSet(&g_Events[MAX_EVENTS], listenFd, AcceptConn, &g_Events[MAX_EVENTS]);    
  179.     // add listen socket    
  180.     EventAdd(epollFd, EPOLLIN, &g_Events[MAX_EVENTS]);    
  181.     // bind & listen    
  182.     sockaddr_in sin;    
  183.     bzero(&sin, sizeof(sin));    
  184.     sin.sin_family = AF_INET;    
  185.     sin.sin_addr.s_addr = INADDR_ANY;    
  186.     sin.sin_port = htons(port);    
  187.     bind(listenFd, (const sockaddr*)&sin, sizeof(sin));    
  188.     listen(listenFd, 5);    
  189. }    
  190. int main(int argc, char **argv)    
  191. {    
  192.     unsigned short port = 12345; // default port    
  193.     if(argc == 2){    
  194.         port = atoi(argv[1]);    
  195.     }    
  196.     // create epoll    
  197.     g_epollFd = epoll_create(MAX_EVENTS);    
  198.     if(g_epollFd <= 0) printf("create epoll failed.%d\n", g_epollFd);    
  199.     // create & bind listen socket, and add to epoll, set non-blocking    
  200.     InitListenSocket(g_epollFd, port);    
  201.     // event loop    
  202.     struct epoll_event events[MAX_EVENTS];    
  203.     printf("server running:port[%d]\n", port);    
  204.     int checkPos = 0;    
  205.     while(1){    
  206.         // a simple timeout check here, every time 100, better to use a mini-heap, and add timer event    
  207.         long now = time(NULL);    
  208.         for(int i = 0; i < 100; i++, checkPos++) // doesn't check listen fd    
  209.         {    
  210.             if(checkPos == MAX_EVENTS) checkPos = 0; // recycle    
  211.             if(g_Events[checkPos].status != 1) continue;    
  212.             long duration = now - g_Events[checkPos].last_active;    
  213.             if(duration >= 60) // 60s timeout    
  214.             {    
  215.                 close(g_Events[checkPos].fd);    
  216.                 printf("[fd=%d] timeout[%d--%d].\n", g_Events[checkPos].fd, g_Events[checkPos].last_active, now);    
  217.                 EventDel(g_epollFd, &g_Events[checkPos]);    
  218.             }    
  219.         }    
  220.         // wait for events to happen    
  221.         int fds = epoll_wait(g_epollFd, events, MAX_EVENTS, 1000);    
  222.         if(fds < 0){    
  223.             printf("epoll_wait error, exit\n");    
  224.             break;    
  225.         }    
  226.         for(int i = 0; i < fds; i++){    
  227.             myevent_s *ev = (struct myevent_s*)events[i].data.ptr;    
  228.             if((events[i].events&EPOLLIN)&&(ev->events&EPOLLIN)) // read event    
  229.             {    
  230.                 ev->call_back(ev->fd, events[i].events, ev->arg);    
  231.             }    
  232.             if((events[i].events&EPOLLOUT)&&(ev->events&EPOLLOUT)) // write event    
  233.             {    
  234.                 ev->call_back(ev->fd, events[i].events, ev->arg);    
  235.             }    
  236.         }    
  237.     }    
  238.     // free resource    
  239.     return 0;    
  240. }    

 

 

 

Mmap的實現原理和應用

不少文章分析了mmap的實現原理。從代碼的邏輯來分析,老是覺沒有把mmap後讀寫映射區域和普通的read/write聯繫起來。不得不產生疑問:

1,普通的read/write和mmap後的映射區域的讀寫到底有什麼區別。

2, 爲何有時候會選擇mmap而放棄普通的read/write。

3,若是文章中的內容有不對是或者是不妥的地方,歡迎你們指正。

 

圍繞着這兩個問題分析一下,其實在考慮這些問題的同時難免和其餘的不少系統機制產生交互。雖然是講解mmap,可是不少知識仍是爲了闡明問題作必要的鋪墊。這些知識也正是linux的繁瑣所在。一個應用每每和系統中的多種機制交互。這篇文章中儘可能減小對源代碼的引用和分析。把這樣的工做留到之後的細節分析中。可是不少分析的理論依據仍是來自於源代碼。可見源代碼的重要地位。

 

基礎知識:

1, 進程每次切換後,都會在tlb base寄存器中從新load屬於每個進程本身的地址轉換基地址。在cpu當前運行的進程中都會有current宏來表示當前的進程的信息。應爲這個代碼實現涉及到硬件架構的問題,爲了不差別的存在在文章中用到硬件知識的時候仍是指明是x86的架構,畢竟x86的資料和分析的研究人員也比較多。其實arm還有其餘相似的RISC的芯片,只要有mmu支持的,都會有相似的基地址寄存器。

2, 在系統運行進程以前都會爲每個進程分配屬於它本身的運行空間。而且這個空間的有效性依賴於tlb base中的內容。32位的系統中訪問的空間大小爲4G。在這個空間中進程是「自由」的。所謂「自由」不是說對於4G的任何一個地址或者一段空間均可以訪問。若是要訪問,仍是要遵循地址有效性,就是tlb base中所指向的任何頁錶轉換後的物理地址。其中的有效性有越界,權限等等檢查。

3, 任何一個用戶進程的運行在系統分配的空間中。這個空間能夠有

vma:struct vm_area_struct來表示。全部的運行空間能夠有這個結構體描述。用戶進程能夠分爲text data 段。這些段的具體在4G中的位置有不一樣的vma來描述。Vma的管理又有其餘機制保證,這些機制涉及到了算法和物理內存管理等。請看一下兩個圖片:

圖 一:

圖 二:

系統調用中的write和read:

 這裏沒有指定確切的文件系統類型做爲分析的對象。找到系統調用號,而後肯定具體的文件系統所帶的file operation。在特定的file operation中有屬於每一種文件系統本身的操做函數集合。其中就有read和write。

圖 三:

在真正的把用戶數據讀寫到磁盤或者是存儲設備前,內核還會在page cache中管理這些數據。這些page的存在有效的管理了用戶數據和讀寫的效率。用戶數據不是直接來自於應用層,讀(read)或者是寫入(write)磁盤和存儲介質,而是被一層一層的應用所劃分,在每一層次中都會有不一樣的功能對應。最後發生交互時,在最恰當的時機觸發磁盤的操做。經過IO驅動寫入磁盤和存儲介質。這裏主要強調page cache的管理。應爲page的管理設計到了緩存,這些緩存以page的單位管理。在沒有IO操做以前,暫時存放在系統空間中,而並未直接寫入磁盤或者存貯介質。

系統調用中的mmap:

當建立一個或者切換一個進程的同時,會把屬於這個當前進程的系統信息載入。這些系統信息中包含了當前進程的運行空間。當用戶程序調用mmap後。函數會在當前進程的空間中找到適合的vma來描述本身將要映射的區域。這個區域的做用就是將mmap函數中文件描述符所指向的具體文件中內容映射過來。

原理是:mmap的執行,僅僅是在內核中創建了文件與虛擬內存空間的對應關係。用戶訪問這些虛擬內存空間時,頁面表裏面是沒有這些空間的表項的。當用戶程序試圖訪問這些映射的空間時,因而產生缺頁異常。內核捕捉這些異常,逐漸將文件載入。所謂的載入過程,具體的操做就是read和write在管理pagecache。Vma的結構體中有很文件操做集。vma操做集中會有本身關於page cache的操做集合。這樣,雖然是兩種不一樣的系統調用,因爲操做和調用觸發的路徑不一樣。可是最後仍是落實到了page cache的管理。實現了文件內容的操做。

Ps:

文件的page cache管理也是很好的內容。涉及到了address space的操做。其中不少的內容和文件操做相關。

效率對比:

    這裏應用了網上一篇文章。發現較好的分析,着這裏引用一下。

Mmap:

#include <stdio.h>

#include <stdlib.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <unistd.h>

#include <sys/mman.h>

void main()

{

int fd = open("test.file", 0);

struct stat statbuf;

char *start;

char buf[2] = {0};

int ret = 0;

fstat(fd, &statbuf);

start = mmap(NULL, statbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0);

do {

*buf = start[ret++];

}while(ret < statbuf.st_size);

}  

Read:

#include <stdio.h>

#include <stdlib.h>

void main()

{

FILE *pf = fopen("test.file", "r");

char buf[2] = {0};

int ret = 0;

do {

ret = fread(buf, 1, 1, pf);

}while(ret);

}   

 

運行結果:

[xiangy@compiling-server test_read]$ time ./fread

real    0m0.901s

user    0m0.892s

sys     0m0.010s

[xiangy@compiling-server test_read]$ time ./mmap

real    0m0.112s

user    0m0.106s

sys     0m0.006s

[xiangy@compiling-server test_read]$ time ./read

real    0m15.549s

user    0m3.933s

sys     0m11.566s

[xiangy@compiling-server test_read]$ ll test.file

-rw-r--r-- 1 xiangy svx8004 23955531 Sep 24 17:17 test.file   

 

能夠看出使用mmap後發現,系統調用所消耗的時間遠遠比普通的read少不少。

 

 

 

共享內存:mmap函數實現

內存映射的應用:

  • 以頁面爲單位,將一個普通文件映射到內存中,一般在須要對文件進行頻繁讀寫時使用,這樣用內存讀寫取代I/O讀寫,以得到較高的性能;
  • 將特殊文件進行匿名內存映射,能夠爲關聯進程提供共享內存空間;
  • 爲無關聯的進程提供共享內存空間,通常也是將一個普通文件映射到內存中。

相關API

#include <sys/mman.h> void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); void *mmap64(void *addr, size_t length, int prot, int flags, int fd, off64_t offset); int munmap(void *addr, size_t length); int msync(void *addr, size_t length, int flags);

mmap函數說明:

  1. 參數 addr 指明文件描述字fd指定的文件在進程地址空間內的映射區的開始地址,必須是頁面對齊的地址,一般設爲 NULL,讓內核去選擇開始地址。任何狀況下,mmap 的返回值爲內存映射區的開始地址。

  2. 參數 length 指明文件須要被映射的字節長度。off 指明文件的偏移量。一般 off 設爲 0 。

    • 若是 len 不是頁面的倍數,它將被擴大爲頁面的倍數。擴充的部分一般被系統置爲 0 ,並且對其修改並不影響到文件。
    • off 一樣必須是頁面的倍數。經過 sysconf(_SC_PAGE_SIZE) 能夠得到頁面的大小。
  3. 參數 prot 指明映射區的保護權限。一般有如下 4 種。一般是 PROT_READ | PROT_WRITE 。

    • PROT_READ 可讀
    • PROT_WRITE 可寫
    • PROT_EXEC 可執行
    • PROT_NONE 不能被訪問
  4. 參數 flag 指明映射區的屬性。取值有如下幾種。MAP_PRIVATE 與 MAP_SHARED 必選其一,MAP_FIXED 爲可選項。

    • MAP_PRIVATE 指明對映射區數據的修改不會影響到真正的文件。
    • MAP_SHARED 指明對映射區數據的修改,多個共享該映射區的進程均可以看見,並且會反映到實際的文件。
    • MAP_FIXED 要求 mmap 的返回值必須等於 addr 。若是不指定 MAP_FIXED 而且 addr 不爲 NULL ,則對 addr 的處理取決於具體實現。考慮到可移植性,addr 一般設爲 NULL ,不指定 MAP_FIXED。
  5. 當 mmap 成功返回時,fd 就能夠關閉,這並不影響建立的映射區。

munmap函數說明:

進程退出的時候,映射區會自動刪除。不過當再也不須要映射區時,能夠調用 munmap 顯式刪除。當映射區刪除後,後續對映射區的引用會生成 SIGSEGV 信號。

msync函數說明:

文件一旦被映射後,調用mmap()的進程對返回地址的訪問是對某一內存區域的訪問,暫時脫離了磁盤上文件的影響。全部對mmap()返回地址空間的操做只在內存中有意義,只有在調用了munmap()後或者msync()時,才把內存中的相應內容寫回磁盤文件。

代碼實例:

兩個進程經過映射普通文件實現共享內存通訊

map_normalfile1.c及map_normalfile2.c。編譯兩個程序,可執行文件分別爲map_normalfile1及map_normalfile2。兩個程序經過命令行參數指定同一個文件來實現共享內存方式的進程間通訊。map_normalfile2試圖打開命令行參數指定的一個普通文件,把該文件映射到進程的地址空間,並對映射後的地址空間進行寫操做。map_normalfile1把命令行參數指定的文件映射到進程地址空間,而後對映射後的地址空間執行讀操做。這樣,兩個進程經過命令行參數指定同一個文件來實現共享內存方式的進程間通訊。

/*-------------map_normalfile1.c-----------*/ #include <stdio.h> #include <string.h> #include <sys/mman.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> typedef struct{ char name[4]; int age; }people; int main(int argc, char** argv) // map a normal file as shared mem: { int fd,i; people *p_map; char temp[2] = {'\0'}; fd=open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777); lseek(fd,sizeof(people)*5-1,SEEK_SET); write(fd,"",1); p_map = (people*) mmap( NULL,sizeof(people)*10,PROT_READ|PROT_WRITE, MAP_SHARED,fd,0 ); close( fd ); temp[0] = 'a'; for(i=0; i<15; i++) { temp[0] += 1; memcpy( ( *(p_map+i) ).name, &temp[0],2 ); ( *(p_map+i) ).age = 20+i; } printf("initialize over\n"); sleep(10); munmap( p_map, sizeof(people)*10 ); printf( "umap ok \n" ); return 0; }

/*-------------map_normalfile2.c-----------*/ #include <stdio.h> #include <string.h> #include <sys/mman.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> typedef struct{ char name[4]; int age; }people; int main(int argc, char** argv) // map a normal file as shared mem: { int fd,i; people *p_map; char temp[2] = {'\0'}; fd=open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777); lseek(fd,sizeof(people)*5-1,SEEK_SET); write(fd,"",1); p_map = (people*) mmap( NULL,sizeof(people)*10,PROT_READ|PROT_WRITE, MAP_SHARED,fd,0 ); close( fd ); temp[0] = 'a'; for(i=0; i<15; i++) { temp[0] += 1; memcpy( ( *(p_map+i) ).name, &temp[0],2 ); ( *(p_map+i) ).age = 20+i; } printf("initialize over\n"); sleep(10); munmap( p_map, sizeof(people)*10 ); printf( "umap ok \n" ); return 0; }

map_normalfile1首先打開或建立一個文件,並把文件的長度設置爲5個people結構大小.mmap映射10個people結構大小的內存,利用返回的地址開始設置15個people結構。而後睡眠10S,等待其餘進程映射同一個文件,而後解除映射。

經過實驗,在map_normalfile1輸出initialize over 以後,輸出umap ok以前,運行map_normalfile2 file,能夠輸出設置好的15個people結構

在map_normalfile1 輸出umap ok後,運行map_normalfile2則輸出結構,前5個people是已設置的,後10結構爲0。

1) 最終被映射文件的內容的長度不會超過文件自己的初始大小,即映射不能改變文件的大小.
2) 能夠用於進程通訊的有效地址空間大小大致上受限於被映射文件的大小,但不徹底受限於文件大小.打開文件的大小爲5個people結構,映射長度爲10個people結構長度,共享內存通訊用15個people結構大小。
在linux中,內存的保護是以頁爲基本單位的,即便被映射文件只有一個字節大小,內核也會爲映射分配一個頁面大小的內存。當被映射文件小於一個頁面大小時,進程能夠對從mmap()返回地址開始的一個頁面大小進行訪問,而不會出錯;可是,若是對一個頁面之外的地址空間進行訪問,則致使錯誤發生,後面將進一步描述。所以,可用於進程間通訊的有效地址空間大小不會超過文件大小及一個頁面大小的和。
3) 文件一旦被映射後,調用mmap()的進程對返回地址的訪問是對某一內存區域的訪問,暫時脫離了磁盤上文件的影響。全部對mmap()返回地址空間的操做只在內存中有意義,只有在調用了munmap()後或者msync()時,才把內存中的相應內容寫回磁盤文件,所寫內容仍然不能超過文件的大小

技巧:
生成固定大小的文件的兩種方式:

/*第一種方法*/ fd = open(PATHNAME, O_RDWR | O_CREAT | O_TRUNC, FILE_MODE); lseek(fd, filesize-1, SEEK_SET); write(fd, "", 1); /*第二種方法*/ ftruncate(fd, filesize);

父子進程經過匿名映射實現共享內存

  1. 匿名內存映射 與 使用 /dev/zero 類型,都不須要真實的文件。要使用匿名映射之須要向 mmap 傳入 MAP_ANON 標誌,而且 fd 參數 置爲 -1 。
  2. 所謂匿名,指的是映射區並無經過 fd 與 文件路徑名相關聯。匿名內存映射用在有血緣關係的進程間。
#include <sys/mman.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> typedef struct{ char name[4]; int age; }people; main(int argc, char** argv) { int i; people *p_map; char temp; p_map=(people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS,-1,0); if(fork() == 0) { sleep(2); for(i = 0;i<5;i++) printf("child read: the %d people's age is %d\n",i+1,(*(p_map+i)).age); (*p_map).age = 100; munmap(p_map,sizeof(people)*10); //實際上,進程終止時,會自動解除映射。 exit(); } temp = 'a'; for(i = 0;i<5;i++) { temp += 1; memcpy((*(p_map+i)).name, &temp,2); (*(p_map+i)).age=20+i; } sleep(5); printf( "parent read: the first people,s age is %d\n",(*p_map).age ); printf("umap\n"); munmap( p_map,sizeof(people)*10 ); printf( "umap ok\n" ); }

參考:

 

 

 

 

驅動總結之mmap函數實現

mmap做爲struct file_operations的重要一個元素,mmap主要是實現物理內存到虛擬內存的映射關係,這樣能夠實現直接訪問虛擬內存,而不用使用設備相關的read、write操做,mmap的基本過程是將文件映射到虛擬內存中。在以前的一篇博客中談到了mmap實現文件複製的操做。

關於linux中的mmap調用以下,最好的辦法查看命令,man mmap:
必要的頭文件
#include<sys/mman.h>
函數聲明
void * mmap(void *addr,size_t length,int prot,
         int flags,int fd,off_t offset);
 
關於各個參數的意義以下:
一、返回值是一個通用型指針,這樣就保證了各類類型的申請方式。
二、void *addr 是程序員所但願的虛擬地址做爲起始映射地址,一般爲NULL,內核自動分配。
三、size_t length固然是指須要映射的區域大小。
四、int flags是指對這段區域的保護方式。具體的能夠參看內核源碼的linux/mm.h。經常使用的是PROT_EXEC,PROT_READ,PROT_WRITE,PROT_NONE。
五、int flags主要是指對這段區域的映射方式,主要分爲兩種方式MAP_SHARE,MAP_PRIVATE.其中的MAP_SHARE是指對映射區域的寫操做會更新到文件中,這樣就至關於直接操做文件。而MAP_PRIVATE一般採用一種稱爲"寫時保護的機制"實現映射,對映射區的寫操做不會更新到文件中,實現方法是將須要被寫操做的頁複製到從新分配的新頁中,而後再對新的頁進行寫操做。原來的映射並無改變,可是讀操做並不會從新分配物理內存空間。具體的參考深刻理解計算機系統。
六、int fd是指將被映射的文件描述符,映射須要保證文件描述符的正確性。
七、off_t offset是指從文件的具體位置開始映射,一般狀況下能夠設置爲0,即從開頭映射。
基本的映射關係以下圖:
 
 
設備驅動的mmap實現主要是將一個物理設備的可操做區域(設備空間)映射到一個進程的虛擬地址空間。這樣就能夠直接採用指針的方式像訪問內存的方式訪問設備。在驅動中的mmap實現主要是完成一件事,就是實際物理設備的操做區域到進程虛擬空間地址的映射過程。同時也須要保證這段映射的虛擬存儲器區域不會被進程當作通常的空間使用,所以須要添加一系列的保護方式。
 
具體的實現過程以下:
  1. /*主要是創建虛擬地址到物理地址的頁表關係,其餘的過程又內核本身完成*/
  2. static int mem_mmap(struct file* filp,struct vm_area_struct *vma)
  3. {
  4.     /*間接的控制設備*/
  5.     struct mem_dev *dev = filp->private_data;
  6.     
  7.     /*標記這段虛擬內存映射爲IO區域,並阻止系統將該區域包含在進程的存放轉存中*/
  8.     vma->vm_flags |= VM_IO;
  9.     /*標記這段區域不能被換出*/
  10.     vma->vm_flags |= VM_RESERVED;
  11.     /**/
  12.     if(remap_pfn_range(vma,/*虛擬內存區域*/
  13.         vma->vm_start/*虛擬地址的起始地址*/
  14.         virt_to_phys(dev->data)>>PAGE_SHIFT/*物理存儲區的物理頁號*/
  15.      dev->size,    /*映射區域大小*/        
  16.         vma->vm_page_prot /*虛擬區域保護屬性*/    
  17.         ))
  18.         return -EAGAIN;
  19.     return 0;
  20. }
具體的實現分析以下:
  1. vma->vm_flags |= VM_IO;
  2. vma->vm_flags |= VM_RESERVED;

上面的兩個保護機制就說明了被映射的這段區域具備映射IO的類似性,同時保證這段區域不能隨便的換出。就是創建一個物理頁與虛擬頁之間的關聯性。具體原理是虛擬頁和物理頁之間是以頁表的方式關聯起來,虛擬內存一般大於物理內存,在使用過程當中虛擬頁經過頁表關聯一切對應的物理頁,當物理頁不夠時,會選擇性的犧牲一些頁,也就是將物理頁與虛擬頁之間切斷,重現關聯其餘的虛擬頁,保證物理內存夠用。在設備驅動中應該具體的虛擬頁和物理頁之間的關係應該是長期的,應該保護起來,不能隨便被別的虛擬頁所替換。具體也可參看關於虛擬存儲器的文章。

接下來就是創建物理頁與虛擬頁之間的關係,即採用函數remap_pfn_range(),具體的參數以下:

int remap_pfn_range(structvm_area_struct *vma, unsigned long addr,unsigned long pfn, unsigned long size, pgprot_t prot)

一、struct vm_area_struct是一個虛擬內存區域結構體,表示虛擬存儲器中的一個內存區域。其中的元素vm_start是指虛擬存儲器中的起始地址。

二、addr也就是虛擬存儲器中的起始地址,一般能夠選擇addr = vma->vm_start。

三、pfn是指物理存儲器的具體頁號,一般經過物理地址獲得對應的物理頁號,具體採用virt_to_phys(dev->data)>>PAGE_SHIFT.首先將虛擬內存轉換到物理內存,而後獲得頁號。>>PAGE_SHIFT一般爲12,這是由於每一頁的大小恰好是4K,這樣右移12至關於除以4096,獲得頁號。

四、size區域大小

五、區域保護機制。

返回值,若是成功返回0,不然正數。

測試代碼能夠直接經過對虛擬內存區域操做,實現不一樣的操做,以下:

  1. #include<fcntl.h>
  2. #include<unistd.h>
  3. #include<stdio.h>
  4. #include<stdlib.h>
  5. #include<sys/types.h>
  6. #include<sys/stat.h>
  7. #include<sys/mman.h>
  8. #include<string.h>
  9. int main()
  10. {
  11.     int fd;
  12.     char *start;
  13.         
  14.     char buf[2048];
  15.     strcpy(buf,"This is a test!!!!");
  16.     fd = open("/dev/memdev0",O_RDWR);
  17.     
  18.     if(fd =-1)
  19.     {
  20.         printf("Error!!\n");
  21.         exit(-1);
  22.     }
  23.     /*建立映射*/
  24.     start = mmap(NULL,2048,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
  25.     /*必須檢測是否成功*/
  26.     if(start =-1)
  27.     {
  28.         printf("mmap error!!!\n");
  29.         exit(-1);
  30.     }
  31.     strcpy(start,buf);
  32.     printf("start = %s,buf = %s\n",start,buf);
  33.     strcpy(start,"Test is Test!!!\n");
  34.     printf("start = %s,buf = %s\n",start,buf);
  35.     /**/
  36.     strcpy(buf,start);
  37.  
  38.     printf("start = %s,buf=%s\n",start,buf);
  39.     /*取消映射關係*/
  40.     munmap(start,2048);
  41.     /*關閉文件*/
  42.     close(fd);
  43.     exit(0);
  44. }

通過測試,成功獲得了驅動。

 

 

 

 

Linux 內存映射函數 mmap()函數詳解

mmap將一個文件或者其它對象映射進內存。文件被映射到多個頁上,若是文件的大小不是全部頁的大小之和,最後一個頁不被使用的空間將會清零。mmap在用戶空間映射調用系統中做用很大。
頭文件 <sys/mman.h>
函數原型
void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);

int munmap(void* start,size_t length);

mmap()[1] 必須以PAGE_SIZE爲單位進行映射,而內存也只能以頁爲單位進行映射,若要映射非PAGE_SIZE整數倍的地址範圍,要先進行內存對齊,強行以PAGE_SIZE的倍數大小進行映射。

用法:

下面說一下內存映射的步驟:
用open系統調用打開文件, 並返回描述符fd.
用mmap創建內存映射, 並返回映射首地址指針start.
對映射(文件)進行各類操做, 顯示(printf), 修改(sprintf).
用munmap(void *start, size_t lenght)關閉內存映射.
用close系統調用關閉文件fd.

UNIX網絡編程第二捲進程間通訊對mmap函數進行了說明。該函數主要用途有三個:
一、將一個普通文件映射到內存中,一般在須要對文件進行頻繁讀寫時使用,這樣用內存讀寫取代I/O讀寫,以得到較高的性能;
二、將特殊文件進行匿名內存映射,能夠爲關聯進程提供共享內存空間;
三、爲無關聯的進程提供共享內存空間,通常也是將一個普通文件映射到內存中。

函數:void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offsize);
參數start:指向欲映射的內存起始地址,一般設爲 NULL,表明讓系統自動選定地址,映射成功後返回該地址。

參數length:表明將文件中多大的部分映射到內存。

參數prot:映射區域的保護方式。能夠爲如下幾種方式的組合:
PROT_EXEC 映射區域可被執行
PROT_READ 映射區域可被讀取
PROT_WRITE 映射區域可被寫入
PROT_NONE 映射區域不能存取

參數flags:影響映射區域的各類特性。在調用mmap()時必需要指定MAP_SHARED 或MAP_PRIVATE。
MAP_FIXED 若是參數start所指的地址沒法成功創建映射時,則放棄映射,不對地址作修正。一般不鼓勵用此旗標。
MAP_SHARED對映射區域的寫入數據會複製迴文件內,並且容許其餘映射該文件的進程共享。
MAP_PRIVATE 對映射區域的寫入操做會產生一個映射文件的複製,即私人的「寫入時複製」(copy on write)對此區域做的任何修改都不會寫回原來的文件內容。
MAP_ANONYMOUS創建匿名映射。此時會忽略參數fd,不涉及文件,並且映射區域沒法和其餘進程共享。
MAP_DENYWRITE只容許對映射區域的寫入操做,其餘對文件直接寫入的操做將會被拒絕。
MAP_LOCKED 將映射區域鎖定住,這表示該區域不會被置換(swap)。

參數fd:要映射到內存中的文件描述符。若是使用匿名內存映射時,即flags中設置了MAP_ANONYMOUS,fd設爲-1。有些系統不支持匿名內存映射,則能夠使用fopen打開/dev/zero文件,而後對該文件進行映射,能夠一樣達到匿名內存映射的效果。

參數offset:文件映射的偏移量,一般設置爲0,表明從文件最前方開始對應,offset必須是分頁大小的整數倍。

返回值:

若映射成功則返回映射區的內存起始地址,不然返回MAP_FAILED(-1),錯誤緣由存於errno 中。

錯誤代碼:

EBADF 參數fd 不是有效的文件描述詞
EACCES 存取權限有誤。若是是MAP_PRIVATE 狀況下文件必須可讀,使用MAP_SHARED則要有PROT_WRITE以及該文件要能寫入。
EINVAL 參數start、length 或offset有一個不合法。
EAGAIN 文件被鎖住,或是有太多內存被鎖住。
ENOMEM 內存不足。

系統調用mmap()用於共享內存的兩種方式:
(1)使用普通文件提供的內存映射:

適用於任何進程之間。此時,須要打開或建立一個文件,而後再調用mmap()

典型調用代碼以下:

fd=open(name, flag, mode); if(fd<0) ...

ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0);

經過mmap()實現共享內存的通訊方式有許多特色和要注意的地方,能夠參看UNIX網絡編程第二卷。

(2)使用特殊文件提供匿名內存映射:

適用於具備親緣關係的進程之間。因爲父子進程特殊的親緣關係,在父進程中先調用mmap(),而後調用 fork()。那麼在調用fork()以後,子進程繼承父進程匿名映射後的地址空間,一樣也繼承mmap()返回的地址,這樣,父子進程就能夠經過映射區 域進行通訊了。注意,這裏不是通常的繼承關係。通常來講,子進程單獨維護從父進程繼承下來的一些變量。而mmap()返回的地址,卻由父子進程共同維護。 對於具備親緣關係的進程實現共享內存最好的方式應該是採用匿名內存映射的方式。此時,沒必要指定具體的文件,只要設置相應的標誌便可。

 

 

 

1、概述
內存映射,簡而言之就是將用戶空間的一段內存區域映射到內核空間,映射成功後,用戶對這段內存區域的修改能夠直接反映到內核空間,一樣,內核空間對這段區域的修改也直接反映用戶空間。那麼對於內核空間<---->用戶空間二者之間須要大量數據傳輸等操做的話效率是很是高的。
如下是一個把廣泛文件映射到用戶空間的內存區域的示意圖。
圖一:



2、基本函數
mmap函數是unix/linux下的系統調用,詳細內容可參考《Unix Netword programming》卷二12.2節。
mmap系統調用並非徹底爲了用於共享內存而設計的。它自己提供了不一樣於通常對普通文件的訪問方式,進程能夠像讀寫內存同樣對普通文件的操做。而Posix或系統V的共享內存IPC則純粹用於共享目的,固然mmap()實現共享內存也是其主要應用之一。
mmap系統調用使得進程之間經過映射同一個普通文件實現共享內存。普通文件被映射到進程地址空間後,進程能夠像訪問普通內存同樣對文件進行訪問,沒必要再調用read(),write()等操做。mmap並不分配空間, 只是將文件映射到調用進程的地址空間裏(可是會佔掉你的 virutal memory), 而後你就能夠用memcpy等操做寫文件, 而不用write()了.寫完後,內存中的內容並不會當即更新到文件中,而是有一段時間的延遲,你能夠調用msync()來顯式同步一下, 這樣你所寫的內容就能當即保存到文件裏了.這點應該和驅動相關。 不過經過mmap來寫文件這種方式沒辦法增長文件的長度, 由於要映射的長度在調用mmap()的時候就決定了.若是想取消內存映射,能夠調用munmap()來取消內存映射

  1. void * mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset)  



mmap用於把文件映射到內存空間中,簡單說mmap就是把一個文件的內容在內存裏面作一個映像。映射成功後,用戶對這段內存區域的修改能夠直接反映到內核空間,一樣,內核空間對這段區域的修改也直接反映用戶空間。那麼對於內核空間<---->用戶空間二者之間須要大量數據傳輸等操做的話效率是很是高的。
start:要映射到的內存區域的起始地址,一般都是用NULL(NULL即爲0)。NULL表示由內核來指定該內存地址

length:要映射的內存區域的大小
prot:指望的內存保護標誌,不能與文件的打開模式衝突。是如下的某個值,能夠經過or運算合理地組合在一塊兒
PROT_EXEC //頁內容能夠被執行
PROT_READ //頁內容能夠被讀取
PROT_WRITE //頁能夠被寫入
PROT_NONE //頁不可訪問
flags:指定映射對象的類型,映射選項和映射頁是否能夠共享。它的值能夠是一個或者多個如下位的組合體
MAP_FIXED :使用指定的映射起始地址,若是由start和len參數指定的內存區重疊於現存的映射空間,重疊部分將會被丟棄。若是指定的起始地址不可用,操做將會失敗。而且起始地址必須落在頁的邊界上。
MAP_SHARED :對映射區域的寫入數據會複製迴文件內, 並且容許其餘映射該文件的進程共享。
MAP_PRIVATE :創建一個寫入時拷貝的私有映射。內存區域的寫入不會影響到原文件。這個標誌和以上標誌是互斥的,只能使用其中一個。
MAP_DENYWRITE :這個標誌被忽略。
MAP_EXECUTABLE :同上
MAP_NORESERVE :不要爲這個映射保留交換空間。當交換空間被保留,對映射區修改的可能會獲得保證。當交換空間不被保留,同時內存不足,對映射區的修改會引發段違例信號。
MAP_LOCKED :鎖定映射區的頁面,從而防止頁面被交換出內存。
MAP_GROWSDOWN :用於堆棧,告訴內核VM系統,映射區能夠向下擴展。
MAP_ANONYMOUS :匿名映射,映射區不與任何文件關聯。
MAP_ANON :MAP_ANONYMOUS的別稱,再也不被使用。
MAP_FILE :兼容標誌,被忽略。
MAP_32BIT :將映射區放在進程地址空間的低2GB,MAP_FIXED指定時會被忽略。當前這個標誌只在x86-64平臺上獲得支持。
MAP_POPULATE :爲文件映射經過預讀的方式準備好頁表。隨後對映射區的訪問不會被頁違例阻塞。
MAP_NONBLOCK :僅和MAP_POPULATE一塊兒使用時纔有意義。不執行預讀,只爲已存在於內存中的頁面創建頁表入口。

fd:文件描述符(由open函數返回)

offset:表示被映射對象(即文件)從那裏開始對映,一般都是用0。 該值應該爲大小爲PAGE_SIZE的整數倍

返回說明
成功執行時,mmap()返回被映射區的指針,munmap()返回0。失敗時,mmap()返回MAP_FAILED[其值爲(void *)-1],munmap返回-1。errno被設爲如下的某個值
EACCES:訪問出錯
EAGAIN:文件已被鎖定,或者太多的內存已被鎖定
EBADF:fd不是有效的文件描述詞
EINVAL:一個或者多個參數無效
ENFILE:已達到系統對打開文件的限制
ENODEV:指定文件所在的文件系統不支持內存映射
ENOMEM:內存不足,或者進程已超出最大內存映射數量
EPERM:權能不足,操做不容許
ETXTBSY:已寫的方式打開文件,同時指定MAP_DENYWRITE標誌
SIGSEGV:試着向只讀區寫入
SIGBUS:試着訪問不屬於進程的內存區

  1. int munmap(void *start, size_t length)   

start:要取消映射的內存區域的起始地址
length:要取消映射的內存區域的大小。
返回說明
成功執行時munmap()返回0。失敗時munmap返回-1.
int msync(const void *start, size_t length, int flags);

對映射內存的內容的更改並不會當即更新到文件中,而是有一段時間的延遲,你能夠調用msync()來顯式同步一下, 這樣你內存的更新就能當即保存到文件裏
start:要進行同步的映射的內存區域的起始地址。
length:要同步的內存區域的大小
flag:flags能夠爲如下三個值之一:
MS_ASYNC : 請Kernel快將資料寫入。
MS_SYNC : 在msync結束返回前,將資料寫入。
MS_INVALIDATE : 讓核心自行決定是否寫入,僅在特殊情況下使用

3、用戶空間和驅動程序的內存映射
3.一、基本過程
首先,驅動程序先分配好一段內存,接着用戶進程經過庫函數mmap()來告訴內核要將多大的內存映射到內核空間,內核通過一系列函數調用後調用對應的驅動程序的file_operation中指定的mmap函數,在該函數中調用remap_pfn_range()來創建映射關係。
3.二、映射的實現
首先在驅動程序分配一頁大小的內存,而後用戶進程經過mmap()將用戶空間中大小也爲一頁的內存映射到內核空間這頁內存上。映射完成後,驅動程序往這段內存寫10個字節數據,用戶進程將這些數據顯示出來。
驅動程序:

  1. #include <linux/miscdevice.h>   
  2. #include <linux/delay.h>   
  3. #include <linux/kernel.h>   
  4. #include <linux/module.h>   
  5. #include <linux/init.h>   
  6. #include <linux/mm.h>   
  7. #include <linux/fs.h>   
  8. #include <linux/types.h>   
  9. #include <linux/delay.h>   
  10. #include <linux/moduleparam.h>   
  11. #include <linux/slab.h>   
  12. #include <linux/errno.h>   
  13. #include <linux/ioctl.h>   
  14. #include <linux/cdev.h>   
  15. #include <linux/string.h>   
  16. #include <linux/list.h>   
  17. #include <linux/pci.h>   
  18. #include <linux/gpio.h>   
  19.   
  20.   
  21. #define DEVICE_NAME "mymap"   
  22.   
  23.   
  24. static unsigned char array[10]={0,1,2,3,4,5,6,7,8,9};   
  25. static unsigned char *buffer;   
  26.   
  27.   
  28. static int my_open(struct inode *inode, struct file *file)   
  29. {   
  30. return 0;   
  31. }   
  32.   
  33.   
  34. static int my_map(struct file *filp, struct vm_area_struct *vma)   
  35. {   
  36. unsigned long page;   
  37. unsigned char i;   
  38. unsigned long start = (unsigned long)vma->vm_start;   
  39. //unsigned long end = (unsigned long)vma->vm_end;   
  40. unsigned long size = (unsigned long)(vma->vm_end - vma->vm_start);   
  41.   
  42. //獲得物理地址   
  43. page = virt_to_phys(buffer);   
  44. //將用戶空間的一個vma虛擬內存區映射到以page開始的一段連續物理頁面上   
  45. if(remap_pfn_range(vma,start,page>>PAGE_SHIFT,size,PAGE_SHARED))//第三個參數是頁幀號,由物理地址右移PAGE_SHIFT獲得   
  46. return -1;   
  47.   
  48. //往該內存寫10字節數據   
  49. for(i=0;i<10;i++)   
  50. buffer[i] = array[i];   
  51.   
  52. return 0;   
  53. }   
  54.   
  55.   
  56. static struct file_operations dev_fops = {   
  57. .owner = THIS_MODULE,   
  58. .open = my_open,   
  59. .mmap = my_map,   
  60. };   
  61.   
  62. static struct miscdevice misc = {   
  63. .minor = MISC_DYNAMIC_MINOR,   
  64. .name = DEVICE_NAME,   
  65. .fops = &dev_fops,   
  66. };   
  67.   
  68.   
  69. static int __init dev_init(void)   
  70. {   
  71. int ret;   
  72.   
  73. //註冊混雜設備   
  74. ret = misc_register(&misc);   
  75. //內存分配   
  76. buffer = (unsigned char *)kmalloc(PAGE_SIZE,GFP_KERNEL);   
  77. //將該段內存設置爲保留   
  78. SetPageReserved(virt_to_page(buffer));   
  79.   
  80. return ret;   
  81. }   
  82.   
  83.   
  84. static void __exit dev_exit(void)   
  85. {   
  86. //註銷設備   
  87. misc_deregister(&misc);   
  88. //清除保留   
  89. ClearPageReserved(virt_to_page(buffer));   
  90. //釋放內存   
  91. kfree(buffer);   
  92. }   
  93.   
  94.   
  95. module_init(dev_init);   
  96. module_exit(dev_exit);   
  97. MODULE_LICENSE("GPL");   
  98. MODULE_AUTHOR("LKN@SCUT");   



應用程序:

  1. #include <unistd.h>   
  2. #include <stdio.h>   
  3. #include <stdlib.h>   
  4. #include <string.h>   
  5. #include <fcntl.h>   
  6. #include <linux/fb.h>   
  7. #include <sys/mman.h>   
  8. #include <sys/ioctl.h>   
  9.   
  10. #define PAGE_SIZE 4096   
  11.   
  12.   
  13. int main(int argc , char *argv[])   
  14. {   
  15. int fd;   
  16. int i;   
  17. unsigned char *p_map;   
  18.   
  19. //打開設備   
  20. fd = open("/dev/mymap",O_RDWR);   
  21. if(fd < 0)   
  22. {   
  23. printf("open fail\n");   
  24. exit(1);   
  25. }   
  26.   
  27. //內存映射   
  28. p_map = (unsigned char *)mmap(0, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED,fd, 0);   
  29. if(p_map == MAP_FAILED)   
  30. {   
  31. printf("mmap fail\n");   
  32. goto here;   
  33. }   
  34.   
  35. //打印映射後的內存中的前10個字節內容   
  36. for(i=0;i<10;i++)   
  37. printf("%d\n",p_map[i]);   
  38.   
  39.   
  40. here:   
  41. munmap(p_map, PAGE_SIZE);   
  42. return 0;   
  43. }   

先加載驅動後執行應用程序,用戶空間打印以下:

 

 

 

 

 

linux內存映射mmap原理分析

一直都對內存映射文件這個概念很模糊,不知道它和虛擬內存有什麼區別,並且映射這個詞也很讓人迷茫,今天終於搞清楚了。。。下面,我先解釋一下我對映射這個詞的理解,再區分一下幾個容易混淆的概念,以後,什麼是內存映射就很明朗了。

 

原理

首先,「映射」這個詞,就和數學課上說的「一一映射」是一個意思,就是創建一種一一對應關係,在這裏主要是隻 硬盤上文件 的位置與進程 邏輯地址空間 中一塊大小相同的區域之間的一一對應,如圖1中過程1所示。這種對應關係純屬是邏輯上的概念,物理上是不存在的,緣由是進程的邏輯地址空間自己就是不存在的。在內存映射的過程當中,並無實際的數據拷貝,文件沒有被載入內存,只是邏輯上被放入了內存,具體到代碼,就是創建並初始化了相關的數據結構(struct address_space),這個過程有系統調用mmap()實現,因此創建內存映射的效率很高。

 

圖1.內存映射原理  

 

既然創建內存映射沒有進行實際的數據拷貝,那麼進程又怎麼能最終直接經過內存操做訪問到硬盤上的文件呢?那就要看內存映射以後的幾個相關的過程了。

 

mmap()會返回一個指針ptr,它指向進程邏輯地址空間中的一個地址,這樣之後,進程無需再調用read或write對文件進行讀寫,而只須要經過ptr就可以操做文件。可是ptr所指向的是一個邏輯地址,要操做其中的數據,必須經過MMU將邏輯地址轉換成物理地址,如圖1中過程2所示。這個過程與內存映射無關。

 

前面講過,創建內存映射並無實際拷貝數據,這時,MMU在地址映射表中是沒法找到與ptr相對應的物理地址的,也就是MMU失敗,將產生一個缺頁中斷,缺頁中斷的中斷響應函數會在swap中尋找相對應的頁面,若是找不到(也就是該文件歷來沒有被讀入內存的狀況),則會經過mmap()創建的映射關係,從硬盤上將文件讀取到物理內存中,如圖1中過程3所示。這個過程與內存映射無關。

 

若是在拷貝數據時,發現物理內存不夠用,則會經過虛擬內存機制(swap)將暫時不用的物理頁面交換到硬盤上,如圖1中過程4所示。這個過程也與內存映射無關。

 

效率

 

從代碼層面上看,從硬盤上將文件讀入內存,都要通過文件系統進行數據拷貝,而且數據拷貝操做是由文件系統和硬件驅動實現的,理論上來講,拷貝數據的效率是同樣的。可是經過內存映射的方法訪問硬盤上的文件,效率要比read和write系統調用高,這是爲何呢?緣由是read()是系統調用,其中進行了數據拷貝,它首先將文件內容從硬盤拷貝到內核空間的一個緩衝區,如圖2中過程1,而後再將這些數據拷貝到用戶空間,如圖2中過程2,在這個過程當中,實際上完成了 兩次數據拷貝 ;而mmap()也是系統調用,如前所述,mmap()中沒有進行數據拷貝,真正的數據拷貝是在缺頁中斷處理時進行的,因爲mmap()將文件直接映射到用戶空間,因此中斷處理函數根據這個映射關係,直接將文件從硬盤拷貝到用戶空間,只進行了 一次數據拷貝 。所以,內存映射的效率要比read/write效率高。

圖2.read系統調用原理

 

下面這個程序,經過read和mmap兩種方法分別對硬盤上一個名爲「mmap_test」的文件進行操做,文件中存有10000個整數,程序兩次使用不一樣的方法將它們讀出,加1,再寫回硬盤。經過對比能夠看出,read消耗的時間將近是mmap的兩到三倍。 

 

  1. #include<unistd.h>  
  2. #include<stdio.h>  
  3. #include<stdlib.h>  
  4. #include<string.h>  
  5. #include<sys/types.h>  
  6. #include<sys/stat.h>  
  7. #include<sys/time.h>  
  8. #include<fcntl.h>  
  9. #include<sys/mman.h>  
  10.    
  11. #define MAX 10000  
  12.    
  13. int main()  
  14. {  
  15. int i=0;  
  16. int count=0, fd=0;  
  17. struct timeval tv1, tv2;  
  18. int *array = (int *)malloc( sizeof(int)*MAX );  
  19.    
  20. /*read*/  
  21.    
  22. gettimeofday( &tv1, NULL );  
  23. fd = open( "mmap_test", O_RDWR );  
  24. if( sizeof(int)*MAX != read( fd, (void *)array, sizeof(int)*MAX ) )  
  25. {  
  26. printf( "Reading data failed.../n" );  
  27. return -1;  
  28. }  
  29. for( i=0; i<MAX; ++i )  
  30.    
  31. ++array[ i ];  
  32. if( sizeof(int)*MAX != write( fd, (void *)array, sizeof(int)*MAX ) )  
  33. {  
  34. printf( "Writing data failed.../n" );  
  35. return -1;  
  36. }  
  37. free( array );  
  38. close( fd );  
  39. gettimeofday( &tv2, NULL );  
  40. printf( "Time of read/write: %dms/n", tv2.tv_usec-tv1.tv_usec );  
  41.    
  42. /*mmap*/  
  43.    
  44. gettimeofday( &tv1, NULL );  
  45. fd = open( "mmap_test", O_RDWR );  
  46. array = mmap( NULL, sizeof(int)*MAX, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0 );  
  47. for( i=0; i<MAX; ++i )  
  48.    
  49. ++array[ i ];  
  50. munmap( array, sizeof(int)*MAX );  
  51. msync( array, sizeof(int)*MAX, MS_SYNC );  
  52. free( array );  
  53. close( fd );  
  54. gettimeofday( &tv2, NULL );  
  55. printf( "Time of mmap: %dms/n", tv2.tv_usec-tv1.tv_usec );  
  56.    
  57. return 0;  
  58. }  

輸出結果:

Time of read/write: 154ms

Time of mmap: 68ms

 

 

 

 

Linux的mmap內存映射機制解析

       在講述文件映射的概念時,不可避免的要牽涉到虛存(SVR 4VM).實際上,文件映射是虛存的中心概念, 文件映射一方面給用戶提供了一組措施,好似用戶將文件映射到本身地址空間的某個部分,使用簡單的內存訪問指令讀寫文件;另外一方面,它也能夠用於內核的基本組織模式,在這種模式種,內核將整個地址空間視爲諸如文件之類的一組不一樣對象的映射.中的傳統文件訪問方式是,首先用open系統調用打開文件,而後使用read, write以及lseek等調用進行順序或者隨即的I/O.這種方式是很是低效的,每一次I/O操做都須要一次系統調用.另外,若是若干個進程訪問同一個文件,每一個進程都要在本身的地址空間維護一個副本,浪費了內存空間.而若是可以經過必定的機制將頁面映射到進程的地址空間中,也就是說首先經過簡單的產生某些內存管理數據結構完成映射的建立.當進程訪問頁面時產生一個缺頁中斷,內核將頁面讀入內存而且更新頁表指向該頁面.並且這種方式很是方便於同一副本的共享.

     VM是面向對象的方法設計的,這裏的對象是指內存對象:內存對象是一個軟件抽象的概念,它描述內存區與後備存儲之間的映射.系統能夠使用多種類型的後備存儲,好比交換空間,本地或者遠程文件以及幀緩存等等. VM系統對它們統一處理,採用同一操做集操做,好比讀取頁面或者回寫頁面等.每種不一樣的後備存儲均可以用不一樣的方法實現這些操做.這樣,系統定義了一套統一的接口,每種後備存儲給出本身的實現方法.這樣,進程的地址空間就被視爲一組映射到不一樣數據對象上的的映射組成.全部的有效地址就是那些映射到數據對象上的地址.這些對象爲映射它的頁面提供了持久性的後備存儲.映射使得用戶能夠直接尋址這些對象.

    值得提出的是, VM體系結構獨立於Unix系統,全部的Unix系統語義,如正文,數據及堆棧區均可以建構在基本VM系統之上.同時, VM體系結構也是獨立於存儲管理的,存儲管理是由操做系統實施的,:究竟採起什麼樣的對換和請求調頁算法,到底是採起分段仍是分頁機制進行存儲管理,到底是如何將虛擬地址轉換成爲物理地址等等(Linux中是一種叫Three Level Page Table的機制),這些都與內存對象的概念無關.

 

1、LinuxVM的實現.

      一個進程應該包括一個mm_struct(memory manage struct), 該結構是進程虛擬地址空間的抽象描述,裏面包括了進程虛擬空間的一些管理信息: start_code, end_code, start_data, end_data, start_brk, end_brk等等信息.另外,也有一個指向進程虛存區表(vm_area_struct: virtual memory area)的指針,該鏈是按照虛擬地址的增加順序排列的.Linux進程的地址空間被分做許多區(vma),每一個區(vma)都對應虛擬地址空間上一段連續的區域, vma是能夠被共享和保護的獨立實體,這裏的vma就是前面提到的內存對象

  下面是vm_area_struct的結構,其中,前半部分是公共的,與類型無關的一些數據成員,:指向mm_struct的指針,地址範圍等等,後半部分則是與類型相關的成員,其中最重要的是一個指向vm_operation_struct向量表的指針vm_ops, vm_pos向量表是一組虛函數,定義了與vma類型無關的接口.每個特定的子類,即每種vma類型都必須在向量表中實現這些操做.這裏包括了: open, close, unmap, protect, sync, nopage, wppage, swapout這些操做

  1. struct vm_area_struct {   
  2.     /*公共的, 與vma類型無關的 */   
  3.     struct mm_struct * vm_mm;   
  4.     unsigned long vm_start;   
  5.     unsigned long vm_end;   
  6.     struct vm_area_struct *vm_next;   
  7.     pgprot_t vm_page_prot;   
  8.     unsigned long vm_flags;   
  9.     short vm_avl_height;   
  10.     struct vm_area_struct * vm_avl_left;   
  11.     struct vm_area_struct * vm_avl_right;   
  12.     struct vm_area_struct *vm_next_share;   
  13.     struct vm_area_struct **vm_pprev_share;   
  14.     /* 與類型相關的 */   
  15.     struct vm_operations_struct * vm_ops;   
  16.     unsigned long vm_pgoff;   
  17.     struct file * vm_file;   
  18.     unsigned long vm_raend;   
  19.     void * vm_private_data;  
  20. };   

vm_ops: open, close, no_page, swapin, swapout……

 

2、驅動中的mmap()函數解析

       設備驅動的mmap實現主要是將一個物理設備的可操做區域(設備空間)映射到一個進程的虛擬地址空間。這樣就能夠直接採用指針的方式像訪問內存的方式訪問設備。在驅動中的mmap實現主要是完成一件事,就是實際物理設備的操做區域到進程虛擬空間地址的映射過程。同時也須要保證這段映射的虛擬存儲器區域不會被進程當作通常的空間使用,所以須要添加一系列的保護方式。

  1. /*主要是創建虛擬地址到物理地址的頁表關係,其餘的過程又內核本身完成*/  
  2. static int mem_mmap(struct file* filp,struct vm_area_struct *vma)  
  3. {  
  4.     /*間接的控制設備*/  
  5.     struct mem_dev *dev = filp->private_data;  
  6.       
  7.     /*標記這段虛擬內存映射爲IO區域,並阻止系統將該區域包含在進程的存放轉存中*/  
  8.     vma->vm_flags |= VM_IO;  
  9.     /*標記這段區域不能被換出*/  
  10.     vma->vm_flags |= VM_RESERVED;  
  11.   
  12.     /**/  
  13.     if(remap_pfn_range(vma,/*虛擬內存區域*/  
  14.         vma->vm_start, /*虛擬地址的起始地址*/  
  15.         virt_to_phys(dev->data)>>PAGE_SHIFT, /*物理存儲區的物理頁號*/  
  16.      dev->size,    /*映射區域大小*/          
  17.         vma->vm_page_prot /*虛擬區域保護屬性*/      
  18.         ))  
  19.         return -EAGAIN;  
  20.   
  21.     return 0;  
  22. }  

具體的實現分析以下:

vma->vm_flags |= VM_IO;
vma->vm_flags |= VM_RESERVED;

上面的兩個保護機制就說明了被映射的這段區域具備映射IO的類似性,同時保證這段區域不能隨便的換出。就是創建一個物理頁與虛擬頁之間的關聯性。具體原理是虛擬頁和物理頁之間是以頁表的方式關聯起來,虛擬內存一般大於物理內存,在使用過程當中虛擬頁經過頁表關聯一切對應的物理頁,當物理頁不夠時,會選擇性的犧牲一些頁,也就是將物理頁與虛擬頁之間切斷,重現關聯其餘的虛擬頁,保證物理內存夠用。在設備驅動中應該具體的虛擬頁和物理頁之間的關係應該是長期的,應該保護起來,不能隨便被別的虛擬頁所替換。具體也可參看關於虛擬存儲器的文章。

接下來就是創建物理頁與虛擬頁之間的關係,即採用函數remap_pfn_range(),具體的參數以下:

int remap_pfn_range(structvm_area_struct *vma, unsigned long addr,unsigned long pfn, unsigned long size, pgprot_t prot)

一、struct vm_area_struct是一個虛擬內存區域結構體,表示虛擬存儲器中的一個內存區域。其中的元素vm_start是指虛擬存儲器中的起始地址。
二、addr也就是虛擬存儲器中的起始地址,一般能夠選擇addr = vma->vm_start。
三、pfn是指物理存儲器的具體頁號,一般經過物理地址獲得對應的物理頁號,具體採用virt_to_phys(dev->data)>>PAGE_SHIFT.首先將虛擬內存轉換到物理內存,而後獲得頁號。>>PAGE_SHIFT一般爲12,這是由於每一頁的大小恰好是4K,這樣右移12至關於除以4096,獲得頁號。
四、size區域大小
五、區域保護機制。
返回值,若是成功返回0,不然正數。

 

3、系統調用mmap函數解析

        介紹完VM的基本概念後,咱們能夠講述mmapmunmap系統調用了.mmap調用實際上就是一個內存對象vma的建立過程,

一、mmap函數

       Linux提供了內存映射函數mmap,它把文件內容映射到一段內存上(準確說是虛擬內存上),經過對這段內存的讀取和修改,實現對文件的讀取和修改 。普通文件被映射到進程地址空間後,進程能夠向訪問普通內存同樣對文件進行訪問,沒必要再調用read(),write()等操做。

先來看一下mmap的函數聲明:

  1. 頭文件:   
  2. <unistd.h>   
  3. <sys/mman.h>   
  4.   
  5. 原型: void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offsize);   
  6.   
  7. /* 
  8. 返回值: 成功則返回映射區起始地址, 失敗則返回MAP_FAILED(-1).  
  9.  
  10. 參數:  
  11.     addr: 指定映射的起始地址, 一般設爲NULL, 由系統指定.  
  12.     length: 將文件的多大長度映射到內存.  
  13.     prot: 映射區的保護方式, 能夠是:  
  14.         PROT_EXEC: 映射區可被執行.  
  15.         PROT_READ: 映射區可被讀取.  
  16.         PROT_WRITE: 映射區可被寫入.  
  17.         PROT_NONE: 映射區不能存取.  
  18.     flags: 映射區的特性, 能夠是:  
  19.         MAP_SHARED: 對映射區域的寫入數據會複製迴文件, 且容許其餘映射該文件的進程共享.  
  20.         MAP_PRIVATE: 對映射區域的寫入操做會產生一個映射的複製(copy-on-write), 對此區域所作的修改不會寫回原文件.  
  21.         此外還有其餘幾個flags不很經常使用, 具體查看linux C函數說明.  
  22.     fd: 由open返回的文件描述符, 表明要映射的文件.  
  23.     offset: 以文件開始處的偏移量, 必須是分頁大小的整數倍, 一般爲0, 表示從文件頭開始映射. 
  24. */  

mmap的做用是映射文件描述符fd指定文件的 [off,off + len]區域至調用進程的[addr, addr + len]的內存區域, 以下圖所示:

mmap系統調用的實現過程是

1.先經過文件系統定位要映射的文件;

2.權限檢查,映射的權限不會超過文件打開的方式,也就是說若是文件是以只讀方式打開,那麼則不容許創建一個可寫映射; 

3.建立一個vma對象,並對之進行初始化; 

4.調用映射文件的mmap函數,其主要工做是給vm_ops向量表賦值;

5.把該vma鏈入該進程的vma鏈表中,若是能夠和先後的vma合併則合併;

6.若是是要求VM_LOCKED(映射區不被換出)方式映射,則發出缺頁請求,把映射頁面讀入內存中.

 

二、munmap函數

      munmap(void * start, size_t length):

      該調用能夠看做是mmap的一個逆過程.它將進程中從start開始length長度的一段區域的映射關閉,若是該區域不是剛好對應一個vma,則有可能會分割幾個或幾個vma.

      msync(void * start, size_t length, int flags):

     把映射區域的修改回寫到後備存儲中.由於munmap時並不保證頁面回寫,若是不調用msync,那麼有可能在munmap後丟失對映射區的修改.其中flags能夠是MS_SYNC, MS_ASYNC, MS_INVALIDATE, MS_SYNC要求回寫完成後才返回, MS_ASYNC發出回寫請求後當即返回, MS_INVALIDATE使用回寫的內容更新該文件的其它映射.該系統調用是經過調用映射文件的sync函數來完成工做的.

     brk(void * end_data_segement):

將進程的數據段擴展到end_data_segement指定的地址,該系統調用和mmap的實現方式十分類似,一樣是產生一個vma,而後指定其屬性.不過在此以前須要作一些合法性檢查,好比該地址是否大於mm->end_code, end_data_segementmm->brk之間是否還存在其它vma等等.經過brk產生的vma映射的文件爲空,這和匿名映射產生的vma類似,關於匿名映射不作進一步介紹.庫函數malloc就是經過brk實現的.


4、實例解析

       下面這個例子顯示了把文件映射到內存的方法,源代碼是:

  1. /************關於本文 檔******************************************** 
  2. *filename: mmap.c 
  3. *purpose: 說明調用mmap把文件映射到內存的方法 
  4. *wrote by: zhoulifa(zhoulifa@163.com) 周立發(http://zhoulifa.bokee.com) 
  5. Linux愛好者 Linux知識傳播者 SOHO族 開發者 最擅長C語言 
  6. *date time:2008-01-27 18:59 上海大雪天,聽說是多年不遇 
  7. *Note: 任何人能夠任意複製代碼並運用這些文檔,固然包括你的商業用途 
  8. * 但請遵循GPL 
  9. *Thanks to: 
  10. *                Ubuntu 本程序在Ubuntu 7.10系統上測試徹底正常 
  11. *                Google.com 我一般經過google搜索發現許多有用的資料 
  12. *Hope:但願愈來愈多的人貢獻本身的力量,爲科學技術發展出力 
  13. * 科技站在巨人的肩膀上進步更快!感謝有開源前輩的貢獻! 
  14. *********************************************************************/  
  15. #include <sys/mman.h> /* for mmap and munmap */  
  16. #include <sys/types.h> /* for open */  
  17. #include <sys/stat.h> /* for open */  
  18. #include <fcntl.h>     /* for open */  
  19. #include <unistd.h>    /* for lseek and write */  
  20. #include <stdio.h>  
  21.   
  22. int main(int argc, char **argv)  
  23. {  
  24.     int fd;  
  25.     char *mapped_mem, * p;  
  26.     int flength = 1024;  
  27.     void * start_addr = 0;  
  28.   
  29.     fd = open(argv[1], O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);  
  30.     flength = lseek(fd, 1, SEEK_END);  
  31.     write(fd, "\0", 1); /* 在文件最後添加一個空字符,以便下面printf正常工做 */  
  32.     lseek(fd, 0, SEEK_SET);  
  33.     mapped_mem = mmap(start_addr, flength, PROT_READ,        //容許讀  
  34.         MAP_PRIVATE,       //不容許其它進程訪問此內存區域  
  35.             fd, 0);  
  36.       
  37.     /* 使用映射區域. */  
  38.     printf("%s\n", mapped_mem); /* 爲了保證這裏工做正常,參數傳遞的文件名最好是一個文本文件 */  
  39.     close(fd);  
  40.     munmap(mapped_mem, flength);  
  41.     return 0;  
  42. }  

編譯運行此程序:

gcc -Wall mmap.c
./a.out text_filename

上面的方法由於用了PROT_READ,因此只能讀取文件裏的內容,不能修改,若是換成PROT_WRITE就能夠修改文件的內容了。又因爲 用了MAAP_PRIVATE因此只能此進程使用此內存區域,若是換成MAP_SHARED,則能夠被其它進程訪問,好比下面的

  1. #include <sys/mman.h> /* for mmap and munmap */  
  2. #include <sys/types.h> /* for open */  
  3. #include <sys/stat.h> /* for open */  
  4. #include <fcntl.h>     /* for open */  
  5. #include <unistd.h>    /* for lseek and write */  
  6. #include <stdio.h>  
  7. #include <string.h> /* for memcpy */  
  8.   
  9. int main(int argc, char **argv)  
  10. {  
  11.     int fd;  
  12.     char *mapped_mem, * p;  
  13.     int flength = 1024;  
  14.     void * start_addr = 0;  
  15.   
  16.     fd = open(argv[1], O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);  
  17.     flength = lseek(fd, 1, SEEK_END);  
  18.     write(fd, "\0", 1); /* 在文件最後添加一個空字符,以便下面printf正常工做 */  
  19.     lseek(fd, 0, SEEK_SET);  
  20.     start_addr = 0x80000;  
  21.     mapped_mem = mmap(start_addr, flength, PROT_READ|PROT_WRITE,        //容許寫入  
  22.         MAP_SHARED,       //容許其它進程訪問此內存區域  
  23.         fd, 0);  
  24.   
  25.     * 使用映射區域. */  
  26.     printf("%s\n", mapped_mem); /* 爲了保證這裏工做正常,參數傳遞的文件名最好是一個文本文 */  
  27.     while((p = strstr(mapped_mem, "Hello"))) { /* 此處來修改文件 內容 */  
  28.         memcpy(p, "Linux", 5);  
  29.         p += 5;  
  30.     }  
  31.       
  32.     close(fd);  
  33.     munmap(mapped_mem, flength);  
  34.     return 0;  
  35. }  


5、mmap和共享內存對比

      共享內存容許兩個或多個進程共享一給定的存儲區,由於數據不須要來回複製,因此是最快的一種進程間通訊機制。共享內存能夠經過mmap()映射普通文件(特殊狀況下還能夠採用匿名映射)機制實現,也能夠經過系統V共享內存機制實現。應用接口和原理很簡單,內部機制複雜。爲了實現更安全通訊,每每還與信號燈等同步機制共同使用。

對好比下:

      mmap機制:就是在磁盤上創建一個文件,每一個進程存儲器裏面,單獨開闢一個空間來進行映射。若是多進程的話,那麼不會對實際的物理存儲器(主存)消耗太大。

      shm機制:每一個進程的共享內存都直接映射到實際物理存儲器裏面。

一、mmap保存到實際硬盤,實際存儲並無反映到主存上。優勢:儲存量能夠很大(多於主存);缺點:進程間讀取和寫入速度要比主存的要慢。

二、shm保存到物理存儲器(主存),實際的儲存量直接反映到主存上。優勢,進程間訪問速度(讀寫)比磁盤要快;缺點,儲存量不能很是大(多於主存)

使用上看:若是分配的存儲量不大,那麼使用shm;若是存儲量大,那麼使用mmap。

 

 

 

 

mmap - 用戶空間與內核空間

mmap概述

共享內存能夠說是最有用的進程間通訊方式,也是最快的IPC形式, 由於進程能夠直接讀寫內存,而不須要任何數據的拷貝。對於像管道和消息隊列等通訊方式,則須要在內核和用戶空間進行四次的數據拷貝,而共享內存則只拷貝兩次數據: 一次從輸入文件到共享內存區,另外一次從共享內存區到輸出文件。實際上,進程之間在共享內存時,並不老是讀寫少許數據後就解除映射,有新的通訊時,再從新創建共享內存區域。而是保持共享區域,直到通訊完畢爲止,這樣,數據內容一直保存在共享內存中,並無寫回文件。共享內存中的內容每每是在解除映射時才寫回文件的。所以,採用共享內存的通訊方式效率是很是高的。

傳統文件訪問
UNIX訪問文件的傳統方法是用open打開它們, 若是有多個進程訪問同一個文件, 則每個進程在本身的地址空間都包含有該文件的副本,這沒必要要地浪費了存儲空間. 下圖說明了兩個進程同時讀一個文件的同一頁的情形. 系統要將該頁從磁盤讀到高速緩衝區中, 每一個進程再執行一個存儲器內的複製操做將數據從高速緩衝區讀到本身的地址空間.
這裏寫圖片描述

共享存儲映射
如今考慮另外一種處理方法: 進程A和進程B都將該頁映射到本身的地址空間, 當進程A第一次訪問該頁中的數據時, 它生成一個缺頁中斷. 內核此時讀入這一頁到內存並更新頁表使之指向它.之後, 當進程B訪問同一頁面而出現缺頁中斷時, 該頁已經在內存, 內核只須要將進程B的頁表登記項指向次頁便可. 以下圖所示:
這裏寫圖片描述

mmap系統調用使得進程之間經過映射同一個普通文件實現共享內存,普通文件被映射到進程地址空間後,進程能夠像訪問普通內存同樣對文件進行訪問,沒必要再調用read和write等。
這裏寫圖片描述


mmap用戶空間

用戶空間mmap函數原型

頭文件 sys/mman.h
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *start, size_t length);
int msync ( void * addr , size_t len, int flags) 經過調用msync()實現磁盤上文件內容與共享內存區的內容一致

做用:
mmap將一個文件或者其餘對象映射進內存,當文件映射到進程後,就能夠直接操做這段虛擬地址進行文件的讀寫等操做。

參數說明:
start:映射區的開始地址
length:映射區的長度
prot:指望的內存保護標誌
—-PROT_EXEC //頁內容能夠被執行
—-PROT_READ //頁內容能夠被讀取
—-PROT_WRITE //頁能夠被寫入
—-PROT_NONE //頁不可訪問
flags:指定映射對象的類型
—-MAP_FIXED
—-MAP_SHARED 與其它全部映射這個對象的進程共享映射空間
—-MAP_PRIVATE 創建一個寫入時拷貝的私有映射。內存區域的寫入不會影響到原文件
—-MAP_ANONYMOUS 匿名映射,映射區不與任何文件關聯
fd:若是MAP_ANONYMOUS被設定,爲了兼容問題,其值應爲-1
offset:被映射對象內容的起點


經過共享映射的方式修改文件

系統調用mmap能夠將文件映射至內存(進程空間),如此能夠把對文件的操做轉爲對內存的操做,以此避免更多的lseek()、read()、write()等系統調用,這點對於大文件或者頻繁訪問的文件尤爲有用,提升了I/O效率。
下面例子中測試所需的data.txt文件內容以下:

aaaaaaaaa
bbbbbbbbb
ccccccccc
ddddddddd

 

/* * mmap file to memory * ./mmap1 data.txt */ #include <stdio.h> #include <sys/stat.h> #include <sys/mman.h> #include <fcntl.h> #include <stdlib.h> int main(int argc, char *argv[]) { int fd = -1; struct stat sb; char *mmaped = NULL; fd = open(argv[1], O_RDWR); if (fd < 0) { fprintf(stderr, "open %s fail\n", argv[1]); exit(-1); } if (stat(argv[1], &sb) < 0) { fprintf(stderr, "stat %s fail\n", argv[1]); goto err; } /* 將文件映射至進程的地址空間 */ mmaped = (char *)mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (mmaped == (char *)-1) { fprintf(stderr, "mmap fail\n"); goto err; } /* 映射完後, 關閉文件也能夠操縱內存 */ close(fd); printf("%s", mmaped); mmaped[5] = '$'; if (msync(mmaped, sb.st_size, MS_SYNC) < 0) { fprintf(stderr, "msync fail\n"); goto err; } return 0; err: if (fd > 0) close(fd); if (mmaped != (char *)-1) munmap(mmaped, sb.st_size); return -1; }

 


經過共享映射實現兩個進程之間的通訊

兩個程序映射同一個文件到本身的地址空間, 進程A先運行, 每隔兩秒讀取映射區域, 看是否發生變化.
進程B後運行, 它修改映射區域, 而後推出, 此時進程A可以觀察到存儲映射區的變化
進程A的代碼:

#include <sys/mman.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <error.h> #define BUF_SIZE 100 int main(int argc, char **argv) { int fd, nread, i; struct stat sb; char *mapped, buf[BUF_SIZE]; for (i = 0; i < BUF_SIZE; i++) { buf[i] = '#'; } /* 打開文件 */ if ((fd = open(argv[1], O_RDWR)) < 0) { perror("open"); } /* 獲取文件的屬性 */ if ((fstat(fd, &sb)) == -1) { perror("fstat"); } /* 將文件映射至進程的地址空間 */ if ((mapped = (char *)mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == (void *)-1) { perror("mmap"); } /* 文件已在內存, 關閉文件也能夠操縱內存 */ close(fd); /* 每隔兩秒查看存儲映射區是否被修改 */ while (1) { printf("%s\n", mapped); sleep(2); } return 0; } 

 

進程B的代碼:

#include <sys/mman.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <error.h> #define BUF_SIZE 100 int main(int argc, char **argv) { int fd, nread, i; struct stat sb; char *mapped, buf[BUF_SIZE]; for (i = 0; i < BUF_SIZE; i++) { buf[i] = '#'; } /* 打開文件 */ if ((fd = open(argv[1], O_RDWR)) < 0) { perror("open"); } /* 獲取文件的屬性 */ if ((fstat(fd, &sb)) == -1) { perror("fstat"); } /* 私有文件映射將沒法修改文件 */ if ((mapped = (char *)mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0)) == (void *)-1) { perror("mmap"); } /* 映射完後, 關閉文件也能夠操縱內存 */ close(fd); /* 修改一個字符 */ mapped[20] = '9'; return 0; } 

 


經過匿名映射實現父子進程通訊

#include <sys/mman.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #define BUF_SIZE 100 int main(int argc, char** argv) { char *p_map; /* 匿名映射,建立一塊內存供父子進程通訊 */ p_map = (char *)mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); if(fork() == 0) { sleep(1); printf("child got a message: %s\n", p_map); sprintf(p_map, "%s", "hi, dad, this is son"); munmap(p_map, BUF_SIZE); //實際上,進程終止時,會自動解除映射。 exit(0); } sprintf(p_map, "%s", "hi, this is father"); sleep(2); printf("parent got a message: %s\n", p_map); return 0; } 

 


對mmap返回地址的訪問

linux採用的是頁式管理機制。對於用mmap()映射普通文件來講,進程會在本身的地址空間新增一塊空間,空間大
小由mmap()的len參數指定,注意,進程並不必定可以對所有新增空間都能進行有效訪問。進程可以訪問的有效地址大小取決於文件被映射部分的大小。簡單的說,可以容納文件被映射部分大小的最少頁面個數決定了進程從mmap()返回的地址開始,可以有效訪問的地址空間大小。超過這個空間大小,內核會根據超過的嚴重程度返回發送不一樣的信號給進程。可用以下圖示說明:
這裏寫圖片描述
總結一下就是, 文件大小, mmap的參數 len 都不能決定進程能訪問的大小, 而是容納文件被映射部分的最小頁面數決定進程能訪問的大小. 下面看一個實例:

#include <sys/mman.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> int main(int argc, char** argv) { int fd,i; int pagesize,offset; char *p_map; struct stat sb; /* 取得page size */ pagesize = sysconf(_SC_PAGESIZE); printf("pagesize is %d\n",pagesize); /* 打開文件 */ fd = open(argv[1], O_RDWR, 00777); fstat(fd, &sb); printf("file size is %zd\n", (size_t)sb.st_size); offset = 0; p_map = (char *)mmap(NULL, pagesize * 2, PROT_READ|PROT_WRITE, MAP_SHARED, fd, offset); close(fd); p_map[sb.st_size] = '9'; /* 致使總線錯誤 */ p_map[pagesize] = '9'; /* 致使段錯誤 */ munmap(p_map, pagesize * 2); return 0; } 

 


mmap內核空間

內核空間mmap函數原型

內核空間的mmap函數原型爲:int (*map)(struct file *filp, struct vm_area_struct *vma);
做用是實現用戶進程中的地址與內核中物理頁面的映射


mmap函數實現步驟

內核空間mmap函數具體實現步驟以下:
1. 經過kmalloc, get_free_pages, vmalloc等分配一段虛擬地址
2. 若是是使用kmalloc, get_free_pages分配的虛擬地址,那麼使用virt_to_phys()將其轉化爲物理地址,再將獲得的物理地址經過」phys>>PAGE_SHIFT」獲取其對應的物理頁面幀號。或者直接使用virt_to_page從虛擬地址獲取獲得對應的物理頁面幀號。
若是是使用vmalloc分配的虛擬地址,那麼使用vmalloc_to_pfn獲取虛擬地址對應的物理頁面的幀號。
3. 對每一個頁面調用SetPageReserved()標記爲保留才能夠。
4. 經過remap_pfn_range爲物理頁面的幀號創建頁表,並映射到用戶空間。
說明:kmalloc, get_free_pages, vmalloc分配的物理內存頁面最好仍是不要用remap_pfn_range,建議使用VMA的nopage方法。

說明:
若共享小塊連續內存,上面所說的get_free_pages就能夠分配多達幾M的連續空間,
若共享大塊連續內存,就得靠uboot幫忙,給linux kernel傳遞參數的時候指定」mem=」,而後在內核中使用下面兩個函數來預留和釋放內存。
void *alloc_bootmem(unsigned long size);
void free_bootmem(unsigned long addr, unsigned long size);


mmap函數實現例子

在字符設備驅動中,有一個struct file_operation結構提,其中fops->mmap指向你本身的mmap鉤子函數,用戶空間對一個字符設備文件進行mmap系統調用後,最終會調用驅動模塊裏的mmap鉤子函數。在mmap鉤子函數中須要調用下面這個API:

int remap_pfn_range(struct vm_area_struct *vma, //這個結構很重要!!後面講 unsigned long virt_addr, //要映射的範圍的首地址 unsigned long pfn, //要映射的範圍對應的物理內存的頁幀號!!重要 unsigned long size, //要映射的範圍的大小 pgprot_t prot); //PROTECT屬性,mmap()中來的

 

在mmap鉤子函數中,像下面這樣就能夠了

int my_mmap(struct file *filp, struct vm_area_struct *vma){ //......省略,page很重要,其餘的參數通常照下面就能夠了 remap_pfn_range(vma, vma->vm_start, page, (vma->vm_end - vma->vm_start), vma->vm_page_prot); //......省略 }

 

來看一個例子:
內核空間代碼mymap.c

#include <linux/miscdevice.h> #include <linux/delay.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/init.h> #include <linux/mm.h> #include <linux/fs.h> #include <linux/types.h> #include <linux/delay.h> #include <linux/moduleparam.h> #include <linux/slab.h> #include <linux/errno.h> #include <linux/ioctl.h> #include <linux/cdev.h> #include <linux/string.h> #include <linux/list.h> #include <linux/pci.h> #include <linux/gpio.h> #define DEVICE_NAME "mymap" static unsigned char array[10]={0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; static unsigned char *buffer; static int my_open(struct inode *inode, struct file *file) { return 0; } static int my_map(struct file *filp, struct vm_area_struct *vma) { unsigned long phys; //獲得物理地址 phys = virt_to_phys(buffer); //將用戶空間的一個vma虛擬內存區映射到以page開始的一段連續物理頁面上 if(remap_pfn_range(vma, vma->vm_start, phys >> PAGE_SHIFT,//第三個參數是頁幀號,由物理地址右移PAGE_SHIFT得>到 vma->vm_end - vma->vm_start, vma->vm_page_prot)) return -1; return 0; } static struct file_operations dev_fops = { .owner = THIS_MODULE, .open = my_open, .mmap = my_map, }; static struct miscdevice misc = { .minor = MISC_DYNAMIC_MINOR, .name = DEVICE_NAME, .fops = &dev_fops, }; static ssize_t hwrng_attr_current_show(struct device *dev, struct device_attribute *attr, char *buf) { int i; for(i = 0; i < 10 ; i++){ printk("%d\n", buffer[i]); } return 0; } static DEVICE_ATTR(rng_current, S_IRUGO | S_IWUSR, hwrng_attr_current_show, NULL); static int __init dev_init(void) { int ret; unsigned char i; //內存分配 buffer = (unsigned char *)kmalloc(PAGE_SIZE,GFP_KERNEL); //driver起來時初始化內存前10個字節數據 for(i = 0;i < 10;i++) buffer[i] = array[i]; //將該段內存設置爲保留 SetPageReserved(virt_to_page(buffer)); //註冊混雜設備 ret = misc_register(&misc); ret = device_create_file(misc.this_device, &dev_attr_rng_current); return ret; } static void __exit dev_exit(void) { device_remove_file(misc.this_device, &dev_attr_rng_current); //註銷設備 misc_deregister(&misc); //清除保留 ClearPageReserved(virt_to_page(buffer)); //釋放內存 kfree(buffer); } module_init(dev_init); module_exit(dev_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("LKN@SCUT");

 

用戶空間代碼mymap_app.c

/* * /home/lei_wang/xxx/xxx_linux/toolchain/xxx/bin/xxx-linux-gcc mymap_app.c -o mymap_app */ #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <linux/fb.h> #include <sys/mman.h> #include <sys/ioctl.h> #include <errno.h> #define PAGE_SIZE 4096 int main(int argc , char *argv[]) { int fd; int i; unsigned char *p_map; //打開設備 fd = open("/dev/mymap",O_RDWR); if(fd < 0) { printf("open fail\n"); exit(1); } //內存映射 p_map = (unsigned char *)mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if(p_map == (void *)-1) { printf("mmap fail\n"); goto here; } close(fd); //打印映射後的內存中的前10個字節內容, //並將前10個字節中的內容都加上10,寫入內存中 //經過cat cat /sys/devices/virtual/misc/mymap/rng_current查看內存是否被修改 for(i = 0;i < 10;i++) { printf("%d\n",p_map[i]); p_map[i] = p_map[i] + 10; } here: munmap(p_map, PAGE_SIZE); return 0; }

 

這裏寫圖片描述 
從上面這張圖能夠看出:
當系統開機,driver起來的時候會將內存前10個字節初始化,經過cat /sys/devices/virtual/misc/mymap/rng_current,能夠看出此時內存中的值。
當執行mymap_app時會將前10個字節的內容加上10再寫進內存,再經過cat /sys/devices/virtual/misc/mymap/rng_current,能夠看出修改後的內存中的值。


參考文章

  1. linux 內存映射 remap_pfn_range操做
  2. mmap詳解

資源下載

    1. mmap內核驅動與應用程序

 

 

 

 

Linux設備驅動之mmap設備操做

1.mmap系統調用

void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);

功能:負責把文件內容映射到進程的虛擬地址空間,經過對這段內存的讀取和修改來實現對文件的讀取和修改,而不須要再調用read和write;
參數:addr:映射的起始地址,設爲NULL由系統指定;
len:映射到內存的文件長度;
prot:指望的內存保護標誌,不能與文件的打開模式衝突。PROT_EXEC,PROT_READ,PROT_WRITE等;
flags:指定映射對象的類型,映射選項和映射頁是否能夠共享。MAP_SHARED,MAP_PRIVATE等;
fd:由open返回的文件描述符,表明要映射的文件;
offset:開始映射的文件的偏移。
返回值:成功執行時,mmap()返回被映射區的指針。失敗時,mmap()返回MAP_FAILED。

mmap映射圖:


2.解除映射:
 int munmap(void *start, size_t length); 

3.虛擬內存區域:
虛擬內存區域是進程的虛擬地址空間中的一個同質區間,即具備一樣特性的連續地址範圍。一個進程的內存映象由下面幾個部分組成:程序代碼、數據、BSS和棧區域,以及內存映射的區域。
linux內核使用vm_area_struct結構來描述虛擬內存區。其主要成員:

unsigned long vm_start; /* Our start address within vm_mm. */ unsigned long vm_end; /* The first byte after our end address within vm_mm. */ unsigned long vm_flags; /* Flags, see mm.h. 該區域的標記。如VM_IO(該VMA標記爲內存映射的IO區域,會阻止系統將該區域包含在進程的存放轉存中)和VM_RESERVED(標誌內存區域不能被換出)。*/


4.mmap設備操做:
映射一個設備是指把用戶空間的一段地址(虛擬地址區間)關聯到設備內存上,當程序讀寫這段用戶空間的地址時,它其實是在訪問設備。
mmap方法是file_operations結構的成員,在mmap系統調用的發出時被調用。在此以前,內核已經完成了不少工做。
mmap設備方法所須要作的就是創建虛擬地址到物理地址的頁表(虛擬地址和設備的物理地址的關聯經過頁表)。

static int mmap(struct file *file, struct vm_area_struct *vma);


mmap如何完成頁表的創建?(兩種方法)
(1)使用remap_pfn_range一次創建全部頁表。

複製代碼
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr, unsigned long pfn, unsigned long size, pgprot_t prot); /** * remap_pfn_range - remap kernel memory to userspace * @vma: user vma to map to:內核找到的虛擬地址區間 * @addr: target user address to start at:要關聯的虛擬地址 * @pfn: physical address of kernel memory:要關聯的設備的物理地址,也即要映射的物理地址所在的物理幀號,可將物理地址>>PAGE_SHIFT * @size: size of map area * @prot: page protection flags for this mapping * * Note: this is only safe if the mm semaphore is held when called. */
複製代碼


(2)使用nopage VMA方法每次創建一個頁表;

 

5.源碼分析:

(1)memdev.h

複製代碼
View Code
/*mem設備描述結構體*/ struct mem_dev { char *data; unsigned long size; }; #endif /* _MEMDEV_H_ */
複製代碼


(2)memdev.c

複製代碼
View Code
static int mem_major = MEMDEV_MAJOR; module_param(mem_major, int, S_IRUGO); struct mem_dev *mem_devp; /*設備結構體指針*/ struct cdev cdev; /*文件打開函數*/ int mem_open(struct inode *inode, struct file *filp) { struct mem_dev *dev; /*獲取次設備號*/ int num = MINOR(inode->i_rdev); if (num >= MEMDEV_NR_DEVS) return -ENODEV; dev = &mem_devp[num]; /*將設備描述結構指針賦值給文件私有數據指針*/ filp->private_data = dev; return 0; } /*文件釋放函數*/ int mem_release(struct inode *inode, struct file *filp) { return 0; } static int memdev_mmap(struct file*filp, struct vm_area_struct *vma) { struct mem_dev *dev = filp->private_data; /*得到設備結構體指針*/ vma->vm_flags |= VM_IO; vma->vm_flags |= VM_RESERVED; if (remap_pfn_range(vma,vma->vm_start,virt_to_phys(dev->data)>>PAGE_SHIFT, vma->vm_end - vma->vm_start, vma->vm_page_prot)) return -EAGAIN; return 0; } /*文件操做結構體*/ static const struct file_operations mem_fops = { .owner = THIS_MODULE, .open = mem_open, .release = mem_release, .mmap = memdev_mmap, }; /*設備驅動模塊加載函數*/ static int memdev_init(void) { int result; int i; dev_t devno = MKDEV(mem_major, 0); /* 靜態申請設備號*/ if (mem_major) result = register_chrdev_region(devno, 2, "memdev"); else /* 動態分配設備號 */ { result = alloc_chrdev_region(&devno, 0, 2, "memdev"); mem_major = MAJOR(devno); } if (result < 0) return result; /*初始化cdev結構*/ cdev_init(&cdev, &mem_fops); cdev.owner = THIS_MODULE; cdev.ops = &mem_fops; /* 註冊字符設備 */ cdev_add(&cdev, MKDEV(mem_major, 0), MEMDEV_NR_DEVS); /* 爲設備描述結構分配內存*/ mem_devp = kmalloc(MEMDEV_NR_DEVS * sizeof(struct mem_dev), GFP_KERNEL); if (!mem_devp) /*申請失敗*/ { result = - ENOMEM; goto fail_malloc; } memset(mem_devp, 0, sizeof(struct mem_dev)); /*爲設備分配內存*/ for (i=0; i < MEMDEV_NR_DEVS; i++) { mem_devp[i].size = MEMDEV_SIZE; mem_devp[i].data = kmalloc(MEMDEV_SIZE, GFP_KERNEL); memset(mem_devp[i].data, 0, MEMDEV_SIZE); } return 0; fail_malloc: unregister_chrdev_region(devno, 1); return result; } /*模塊卸載函數*/ static void memdev_exit(void) { cdev_del(&cdev); /*註銷設備*/ kfree(mem_devp); /*釋放設備結構體內存*/ unregister_chrdev_region(MKDEV(mem_major, 0), 2); /*釋放設備號*/ } MODULE_AUTHOR("David Xie"); MODULE_LICENSE("GPL"); module_init(memdev_init); module_exit(memdev_exit);
複製代碼


(3)app-mmap.c

複製代碼
#include <stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<sys/mman.h>

int main()
{
    int fd; char *start; //char buf[100]; char *buf; /*打開文件*/ fd = open("/dev/memdev0",O_RDWR); buf = (char *)malloc(100); memset(buf, 0, 100); start=mmap(NULL,100,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); /* 讀出數據 */ strcpy(buf,start); sleep (1); printf("buf 1 = %s\n",buf); /* 寫入數據 */ strcpy(start,"Buf Is Not Null!"); memset(buf, 0, 100); strcpy(buf,start); sleep (1); printf("buf 2 = %s\n",buf); munmap(start,100); /*解除映射*/ free(buf); close(fd); return 0; }
複製代碼


測試步驟:

(1)編譯安裝內核模塊:insmod memdev.ko

(2)查看設備名、主設備號:cat /proc/devices

(3)手工建立設備節點:mknod  /dev/memdev0  c  ***  0

  查看設備文件是否存在:ls -l /dev/* | grep memdev

(4)編譯下載運行應用程序:./app-mmap

  結果:buf 1 = 

     buf 2 = Buf Is Not Null!

 

  總結:mmap設備方法實現將用戶空間的一段內存關聯到設備內存上,對用戶空間的讀寫就至關於對字符設備的讀寫;不是全部的設備都能進行mmap抽象,好比像串口和其餘面向流的設備就不能作mmap抽象。

 

 

 

 

細說linux IPC(三):mmap系統調用共享內存
       前面講到socket的進程間通訊方式,這種方式在進程間傳遞數據時首先須要從進程1地址空間中把數據拷貝到內核,內核再將數據拷貝到進程2的地址空間中,也就是數據傳遞須要通過內核傳遞。這樣在處理較多數據時效率不是很高,而讓多個進程共享一片內存區則解決了以前socket進程通訊的問題。共享內存是最快的進程間通訊 ,將一片內存映射到多個進程地址空間中,那麼進程間的數據傳遞將不在涉及內核。
        共享內存並非從某一進程擁有的內存中劃分出來的;進程的內存老是私有的。共享內存是從系統的空閒內存池中分配的,但願訪問它的每一個進程鏈接它。這個鏈接過程稱爲映射,它給共享內存段分配每一個進程的地址空間中的本地地址。
    mmap()系統調用使得進程之間經過映射同一個普通文件實現共享內存。普通文件被映射到進程地址空間後,進程能夠向訪問普通內存同樣對文件進行訪問,沒必要再調用read(),write()等操做。函數原型爲:
  1. #include <sys/mman.h>  
  2. void *mmap(void *addr, size_t length, int prot, int flags,  int fd, off_t offset);   
        其中參數addr爲描述符fd應該被映射到進程空間的起始地址,當指定爲NULL時內核將本身去選擇起始地址,不管addr是爲NULL,函數返回值都是fd所映射到內存的起始地址;
        len是映射到調用進程地址空間的字節數,它 從被映射文件開頭offset個字節開始算起,offset一般設置爲0;
        prot 參數指定共享內存的訪問權限。可取以下幾個值的或:PROT_READ(可讀) , PROT_WRITE (可寫), PROT_EXEC (可執行), PROT_NONE(不可訪問),該值常設置爲PROT_READ | PROT_WRITE 。
        flags由如下幾個常值指定:MAP_SHARED , MAP_PRIVATE , MAP_FIXED,其中,MAP_SHARED(變更是共享的,對共享內存的修改全部進程可見) , MAP_PRIVATE(變更是私有的,對共享內存修改只對該進程可見)   必選其一,而MAP_FIXED則不推薦使用 。

    munmp() 刪除地址映射關係,函數原型以下:
  1. #include <sys/mman.h>  
  2.   int munmap(void *addr, size_t length);  
        參數addr是由mmap返回的地址,len是映射區大小。

        進程在映射空間的對共享內容的改變並不直接寫回到磁盤文件中,每每在調用munmap()後才執行該操做。能夠經過調用msync()實現磁盤上文件內容與共享內存區的內容一致。 msync()函數原型爲:
  1. #include <sys/mman.h>  
  2. int msync(void *addr, size_t length, int flags);  
        參數addr和len表明內存區,flags有如下值指定,MS_ASYNC(執行異步寫), MS_SYNC(執行同步寫),MS_INVALIDATE(使高速緩存失效)。其中MS_ASYNC和MS_SYNC兩個值必須且只能指定一個,一旦寫操做排入內核,MS_ASYNC當即返回,MS_SYNC要等到寫操做完成後才返回。若是還指定了MS_INVALIDATE,那麼與其最終拷貝不一致的文件數據的全部內存中拷貝都失效。

        在使用open函數打開一個文件以後調用mmap把文件內容映射到調用進程的地址空間,這樣咱們操做文件內容只須要對映射的地址空間進行操做,而無需再使用open,write等函數。

        使用共享內存的步驟基本是:
open()建立內存段;
用 ftruncate()設置它的大小;
用mmap() 把它映射到進程內存,執行其餘參與者須要的操做;
當使用完時,原來的進程調用 munmap()而後退出。
下面來看一個實現:
server程序建立內存並向共享內存寫入數據:
  1. int sln_shm_get(char *shm_file, void **shm, int mem_len)  
  2. {  
  3.     int fd;  
  4.     fd = open(shm_file, O_RDWR | O_CREAT, 0644);//1. 建立內存段  
  5.     if (fd < 0) {  
  6.         printf("open <%s> failed: %s\n", shm_file, strerror(errno));  
  7.         return -1;  
  8.     }  
  9.     ftruncate(fd, mem_len);//2.設置共享內存大小  
  10.     *shm = mmap(NULL, mem_len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); //mmap映射系統內存池到進程內存  
  11.     if (MAP_FAILED == *shm) {  
  12.         printf("mmap: %s\n", strerror(errno));  
  13.         return -1;  
  14.     }  
  15.     close(fd);  
  16.     return 0;  
  17. }  
  18. int main(int argc, const char *argv[])  
  19. {  
  20.     char *shm_buf = NULL;  
  21.     sln_shm_get(SHM_IPC_FILENAME, (void **)&shm_buf, SHM_IPC_MAX_LEN);  
  22.     snprintf(shm_buf, SHM_IPC_MAX_LEN, "hello share memory ipc! i'm server.");  
  23.     return 0;  
  24. }  
client程序映射共享內存並讀取其中數據:
  1. int main(int argc, const char *argv[])  
  2. {  
  3.     char *shm_buf = NULL;  
  4.     sln_shm_get(SHM_IPC_FILENAME, (void **)&shm_buf, SHM_IPC_MAX_LEN);  
  5.     printf("ipc client get: %s\n", shm_buf);  
  6.     munmap(shm_buf, SHM_IPC_MAX_LEN);  
  7.     return 0;  
  8. }  
先執行server程序向共享內存寫入數據,再運行客戶程序,運行結果以下:
  1. # ./server  
  2. # ./client  
  3. ipc client get: hello share memory ipc! i'm server.  
  4. #  
共享內存不像socket那樣自己具備同步機制,它須要經過增長其餘同步操做來實現同步,好比信號量等。同步相關操做在後面會有相關專欄詳細敘述。

 

 

 

存儲器結構、cache、DMA架構分析

存儲器的層次結構
 
 
 
高速緩衝存儲器  cache
 
讀cache操做
 
 
cache若是包含數據就直接從cache中讀出來,由於cache速度要比內存快
若是沒有包含的話,就從內存中找,找到後就放到cache中去,之後再讀的話就直接從cache讀了,下次訪問不少次的時候就會快不少,至關於提升了命中率,cpu的訪問速度就大大提升了
 
cache能大大提升cpu的訪問速率
 
cache的設計
不能太大,也不能過小
 
太大的話,由於程序在查看數據的時候須要把cache走一遍,若是cache太大,
那麼走一遍的時間就太長,就不能達到提高速率的效果
 
若是過小的話,存放在內存的塊數就少了,命中率下降,訪問內存的次數就增長了,cpu性能一樣下降了
 
塊大小
Cache與memory的的數據交換單位
由小變大時, 由局部性原理,命中增長
變得更大時, 新近取得數據被用到的可能性,小於那些必須被移出Cache的數據再次用到的可能性,命中率大大下降
 
 
DMA
 
大量移動數據
 
cpu性能快,外設的速度比較慢,cpu就要一直等待外設
所以誕生了DMA
 
之前沒有DMA,就把數據從內存通過cpu到外設
 
有了DMA的話,cpu把指令告訴DMA,把內存從某個地址開始,多大的數據寫給外設,這樣的話DMA就去作了,cpu就不用作了,DMA就把內存中取出數據,而後再到外設,這種機制就是DMA機制,cpu就解放了,作更重要的事,所以這種內存拷貝、數據大量移動拷貝的時候就用DMA來解放cpu,記住cpu只在開始於結束時參與
 
開始時:
 
結束時:
中斷處理例程
 

 

 

 

直接內存訪問(DMA)

1. 什麼是DMA

直接內存訪問是一種硬件機制,它容許外圍設備和主內存之間直接傳輸它們的I/O數據,而不須要系統處理器的參與。使用這種機制能夠大大提升與設備通訊的吞吐量。

 

2. DMA數據傳輸

有兩種方式引起數據傳輸:

第一種狀況:軟件對數據的請求

1. 當進程調用read,驅動程序函數分配一個DMA緩衝區,並讓硬件將數據傳輸到這個緩衝區中。進程處於睡眠狀態。

2. 硬件將數據寫入到DMA緩衝區中,當寫入完畢,產生一箇中斷

3. 中斷處理程序獲取輸入的數據,應答中斷,並喚起進程,該進程如今便可讀取數據

第二種狀況發生在異步使用DMA時。

1. 硬件產生中斷,宣告新數據的到來

2. 中斷處理程序分配一個緩衝區,而且告訴硬件向哪裏傳輸數據

3. 外圍設備將數據寫入數據區,完成後,產生另一箇中斷

4.處理程序分發新數據,喚醒任何相關進程,而後執行清理工做

 

高效的DMA處理依賴於中斷報告。

 

3. 分配DMA緩衝區

使用DMA緩衝區的主要問題是:當大於一頁時,它們必須佔據連續的物理頁,由於設備使用ISA或PCI系統總線傳輸數據,而這兩種方式使用的都是物理地址。

使用get_free_pasges能夠分配多大幾M字節的內存(MAX_ORDER是11),可是對於較大數量(即便是遠小於128KB)的請求,一般會失敗,這是由於系統內存充滿了內存碎片。

解決方法之一就是在引導時分配內存,或者爲緩衝區保留頂部物理內存。

例子:在系統引導時,向內核傳遞參數「mem=value」的方法保留頂部的RAM。好比系統有256內存,參數「mem=255M」,使內核不能使用頂部的1M字節。隨後,模塊能夠使用下面代碼得到該內存的訪問權:

dmabuf=ioremap(0XFF00000/**255M/, 0X100000/*1M/*);

解決方法之二是使用GPF_NOFAIL分配標誌爲緩衝區分配內存,可是該方法爲內存管理子系統帶來了至關大的壓力。

解決方法之三十設備支持分散/彙集I/O,這能夠將緩衝區分配成多個小塊,設備會很好地處理它們。

 

4. 通用DMA層

DMA操做最終會分配緩衝區,並將總線地址傳遞給設備。內核提升了一個與總線——體系結構無關的DMA層。強烈建議在編寫驅動程序時,爲DMA操做使用該層。使用這些函數的頭文件是。

int dma_set_mask(struct device *dev, u64 mask);

該掩碼顯示該設備能尋址能力對應的位。好比說,設備受限於24位尋址,則mask應該是0x0FFFFFF。

5. DMA映射

IOMMU在設備可訪問的地址範圍內規劃了物理內存,使得物理上分散的緩衝區對設備來講成連續的。對IOMMU的運用須要使用到通用DMA層,而vir_to_bus函數不能完成這個任務。可是,x86平臺沒有對IOMMU的支持。

解決之道就是創建回彈緩衝區,而後,必要時會將數據寫入或者讀出回彈緩衝區。缺點是下降系統性能。

根據DMA緩衝區指望保留的時間長短,PCI代碼區分兩種類型的DMA映射:

一是一致性DMA映射,存在於驅動程序生命週期中,一致性映射的緩衝區必須可同時被CPU和外圍設備訪問。一致性映射必須保存在一致性緩存中。創建和使用一致性映射的開銷是很大的。

二是流式DMA映射,內核開發者建議儘可能使用流式映射,緣由:一是在支持映射寄存器的系統中,每一個DMA映射使用總線上的一個或多個映射寄存器,而一致性映射生命週期很長,長時間佔用這些這些寄存器,甚至在不使用他們的時候也不釋放全部權;二是在一些硬件中,流式映射能夠被優化,但優化的方法對一致性映射無效。

6. 創建一致性映射

驅動程序可調用pci_alloc_consistent函數創建一致性映射:

void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, int falg);

該函數處理了緩衝區的分配和映射,前兩個參數是device結構和所需的緩衝區的大小。函數在兩處返回DMA映射的結果:函數的返回值是緩衝區的內核虛擬地址,能夠被驅動程序使用;而與其相關的總線地址保存在dma_handle中。

當再也不須要緩衝區時,調用下函數:

void dma_free_conherent(struct device *dev, size_t size, void *vaddr, dma_addr_t *dma_handle);

7. DMA池

DMA池是一個生成小型,一致性DMA映射的機制。調用dma_alloc_coherent函數得到的映射,可能其最小大小爲單個頁。若是設備須要的DMA區域比這還小,就是用DMA池。在中定義了DMA池函數:

struct dma_pool *dma_pool_create(const char *name, struct device *dev, size_t size, size_t align, size_t allocation);

void dma_pool_destroy(struct dma_pool *pool);

name是DMA池的名字,dev是device結構,size是從該池中分配的緩衝區的大小,align是該池分配操做所必須遵照的硬件對齊原則(用字節表示),若是allocation不爲零,表示內存邊界不能超越allocation。好比說傳入的allocation是4K,表示從該池分配的緩衝區不能跨越4KB的界限。

在銷燬以前必須向DMA池返回全部分配的內存。

void * dma_pool_alloc(sturct dma_pool *pool, int mem_flags, dma_addr_t *handle);

void dma_pool_free(struct dma_pool *pool, void *addr, dma_addr_t addr);

8. 創建流式DMA映射       

在某些體系結構中,流式映射也可以擁有多個不連續的頁和多個「分散/彙集」緩衝區。創建流式映射時,必須告訴內核數據流動的方向。

DMA_TO_DEVICE

DEVICE_TO_DMA

若是數據被髮送到設備,使用DMA_TO_DEVICE;而若是數據被髮送到CPU,則使用DEVICE_TO_DMA。

DMA_BIDIRECTTONAL

若是數據可雙向移動,則使用該值

DMA_NONE

該符號只是出於調試目的。

當只有一個緩衝區要被傳輸的時候,使用下函數映射它:

dma_addr_t dma_map_single(struct device *dev, void *buffer, size_t size, enum dma_data_direction direction);

返回值是總線地址,能夠把它傳遞給設備;若是執行錯誤,返回NULL。

當傳輸完畢後,使用下函數刪除映射:

void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size, enum dma-data_direction direction);

使用流式DMA的原則:

一是緩衝區只能用於這樣的傳送,即其傳送方向匹配與映射時給定的方向值;

二是一旦緩衝區被映射,它將屬於設備,不是處理器。直到緩衝區被撤銷映射前,驅動程序不能以任何方式訪問其中的內容。只用當dma_unmap_single函數被調用後,顯示刷新處理器緩存中的數據,驅動程序才能安全訪問其中的內容。

三是在DMA出於活動期間內,不能撤銷對緩衝區的映射,不然會嚴重破壞系統的穩定性。             

若是要映射的緩衝區位於設備不能訪問的內存區段(高端內存),怎麼辦?一些體系結構只產生一個錯誤,可是其餘一些系統結構件建立一個回彈緩衝區。回彈緩衝區就是內存中的獨立區域,它可被設備訪問。若是使用DMA_TO_DEVICE標誌映射緩衝區,而且須要使用回彈緩衝區,則在最初緩衝區中的內容做爲映射操做的一部分被拷貝。很明顯,在拷貝後,最初緩衝區內容的改變對設備不可見。一樣DEVICE_TO_DMA回彈緩衝區被dma_unmap_single函數拷貝回最初的緩衝區中,也就是說,直到拷貝操做完成,來自設備的數據纔可用。

有時候,驅動程序須要不通過撤銷映射就訪問流式DMA緩衝區的內容,爲此內核提供了以下調用:

void dma_sync_single_for_cpu(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_directction direction);

應該在處理器訪問流式DMA緩衝區前調用該函數。一旦調用了該函數,處理器將「擁有」DMA緩衝區,並可根據須要對它進行訪問。而後在設備訪問緩衝區前,應該調用下面的函數將全部權交還給設備:

void dma_sync_single_for_device(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_direction direction);

再次強調,處理器在調用該函數後,不能再訪問DMA緩衝區了。

 

 

 

 

DMA原理

DMA原理:DMA 是全部現代電腦的重要特點,他容許不一樣速度的硬件裝置來溝通,而不須要依於 CPU 的大量 中斷 負載。不然,CPU 須要從 來源 把每一片斷的資料複製到 暫存器,而後把他們再次寫回到新的地方。在這個時間中,CPU 對於其餘的工做來講就沒法使用。 DMA 傳輸重要地將一個內存區從一個裝置複製到另一個。當 CPU 初始化這個傳輸動做,傳輸動做自己是由 DMA 控制器 來實行和完成。典型的例子就是移動一個外部內存的區塊到芯片內部更快的內存去。像是這樣的操做並無讓處理器工做拖延,反而能夠被從新排程去處理其餘的工做。DMA 傳輸對於高效能 嵌入式系統 算法和網絡是很重要的。 
  在實現DMA傳輸時,是由DMA控制器直接掌管總線,所以,存在着一個總線控制權轉移問題。即DMA傳輸前,CPU要把總線控制權交給DMA控制器,而在結束DMA傳輸後,DMA控制器應當即把總線控制權再交回給CPU。
DMA - 街角的祝福 -  demo
DMA
一個完整的DMA傳輸過程必須通過下面的4個步驟。
1.DMA請求
  CPU對DMA控制器初始化,並向I/O接口發出操做命令,I/O接口提出DMA請求。
2.DMA響應
  DMA控制器對DMA請求判別優先級及屏蔽,向總線裁決邏輯提出總線請求。當CPU執行完當前總線週期便可釋放總線控制權。此時,總線裁決邏輯輸出總線應答,表示DMA已經響應,經過DMA控制器通知I/O接口開始DMA傳輸。
3.DMA傳輸
  DMA控制器得到總線控制權後,CPU即刻掛起或只執行內部操做,由DMA控制器輸出讀寫命令,直接控制RAM與I/O接口進行DMA傳輸。
4.DMA結束
  當完成規定的成批數據傳送後,DMA控制器即釋放總線控制權,並向I/O接口發出結束信號。當I/O接口收到結束信號後,一方面停 止I/O設備的工做,另外一方面向CPU提出中斷請求,使CPU從不介入的狀態解脫,並執行一段檢查本次DMA傳輸操做正確性的代碼。最後,帶着本次操做結果及狀態繼續執行原來的程序。
  因而可知,DMA傳輸方式無需CPU直接控制傳輸,也沒有中斷處理方式那樣保留現場和恢復現場的過程,經過硬件爲RAM與I/O設備開闢一條直接傳送數據的通路,使CPU的效率大爲提升。

DMA操做模式
    DMA用於無需CPU的介入而直接由專用控制器創建源與目的傳輸的應用,所以,在大量數據傳輸中解放了CPU。PIC32微控制器中的DMA可用於映射到內存空間中的不一樣外設,如從存儲區到SPI,UART或I2C等設備。DMA特性詳見器件參考手冊,這裏僅對一些基本原理與功能作一個簡析。
    PIC32中DMA的傳輸涉及到幾個基本的術語。
    Event:事件,觸發控制器啓動或中止DMA傳輸的操做;
    Transaction:事務,單字傳輸(最多能夠到4個字節),由讀/寫組成;
    Cell transfer:元傳輸,單次共DCHXCSIZE個字節的數據傳輸。元傳輸由單個或多個事務組成。
    Block transfer:塊傳輸,塊傳輸總的字節數由DCHXSSIZ或DCHXDSIZ決定。塊傳輸由單個或多個元傳輸組成。
    事件是觸發DMA控制器產生動做的方式,分爲,START EVENT->啓動傳輸;ABORT EVENT->取消傳輸;STOP EVENT->中止傳輸;爲了有一個完整的概念認識,能夠把用戶軟件的操做,如置位啓動傳輸位等也包含在事件範圍內。由此,能夠看出,任何一個DMA動做都是由事件觸發完成的。用戶在使用DMA控制器時只需設計好事件與DMA操做的關聯便可。要充分的使用DMA控制器,熟悉DMA各類工做模式的原理是頗有必要的。
         傳輸模式二:字符匹配終止模式
    字符匹配模式用於傳輸不定長字節,而又有傳輸終止標識字節的應用環境中,Uart是這種模式的應用案例

     DMA通道的自動使能模式
    DMA每一個通道在正常的塊傳輸、終結字符匹配後或者因異常ABORT後,通道自動禁能。若是該通道有屢次的塊傳輸,須要手動的使能通道;爲了省卻該操做,DCHXCON寄存器提供了容許自動使能通道的位CHAEN(channel auto enable)。通道使能位CHEN在取消傳輸或ABORT事件發生時會被置爲0。
注:
一、通道起始/終止/中止中斷事件獨立於中斷控制器,所以相應的中斷無需使能,也無需在DMA傳輸後清除相應的位;
二、通道優先級和選擇
    DMA控制器每一個通道有一個天然的優先級,CH0默認爲最高,CH4默認爲最低;通道寄存器DCHXCON中提供了修改優先級的控制位。優先級控制了通道的傳輸順序。
 
三、DMA傳輸中的字節對齊
    PIC32採用的數據總線是32位,4字節;無疑訪問地址爲4字節對齊的訪問效率最高,可是,若是把全部的常量或變量存儲地址都限制在4字節對齊顯然是不可能的;DMA中在處理這個問題上採用的字節對齊方法(存儲方式爲LSB)。舉例來講,若是當前物理地址與4的模爲0,則取4字節;模爲1,則取高3字節;模爲2,則取高2字節;模爲3,則取高1字節。
    物理地址爲0x1230,模爲0,則取從0x1230處4字節數據;
    物理地址爲0x1231,模爲1,則取從0x1231處3字節數據;
    物理地址爲0x1232,模爲2,則取從0x1232處2字節數據;
    物理地址爲0x1233,模爲3,則取從0x1233處1字節數據;
    讀/寫過程均採起相同的字節對齊機制。DMA傳輸中的字節對齊過程如圖2.

     直接存儲器存取(DMA)控制器是一種在系統內部轉移數據的獨特外設,能夠將其視爲一種可以經過一組專用總線將內部和外部存儲器與每一個具備DMA能力的外設鏈接起來的控制器。它之因此屬於外設,是由於它是在處理器的編程控制下來執行傳輸的。值得注意的是,一般只有數據流量較大(kBps或者更高)的外設才須要支持DMA能力,這些應用方面典型的例子包括視頻、音頻和網絡接口。

通常而言,DMA控制器將包括一條地址總線、一條數據總線和控制寄存器。高效率的DMA控制器將具備訪問其所須要的任意資源的能力,而無須處理器自己的介入,它必須能產生中斷。最後,它必須能在控制器內部計算出地址。

一個處理器能夠包含多個DMA控制器。每一個控制器有多個DMA通道,以及多條直接與存儲器站(memory bank)和外設鏈接的總線,如圖1所示。在不少高性能處理器中集成了兩種類型的DMA控制器。第一類一般稱爲「系統DMA控制器」,能夠實現對任何資源(外設和存儲器)的訪問,對於這種類型的控制器來講,信號週期數是以系統時鐘(SCLK)來計數的,以ADI的Blackfin處理器爲例,頻率最高可達133MHz。第二類稱爲內部存儲器DMA控制器(IMDMA),專門用於內部存儲器所處位置之間的相互存取操做。由於存取都發生在內部(L1-L一、L1-L2,或者L2-L2),週期數的計數則以內核時鐘(CCLK)爲基準來進行,該時鐘的速度能夠超過600MHz

每一個DMA控制器有一組FIFO,起到DMA子系統和外設或存儲器之間的緩衝器的做用。對於MemDMA(Memory DMA)來講,傳輸的源端和目標端都有一組FIFO存在。當資源緊張而不能完成數據傳輸的話,則FIFO能夠提供數據的暫存區,從而提升性能。

由於一般會在代碼初始化過程當中對DMA控制器進行配置,內核就只須要在數據傳輸完成後對中斷作出響應便可。你能夠對DMA控制進行編程,讓其與內核並行地移動數據,而同時讓內核執行其基本的處理任務—那些應該讓它專一完成的工做。

圖1:系統和存儲器DMA架構.
圖1:系統和存儲器DMA架構。

在一個優化的應用中,內核永遠不用參與任何數據的移動,而僅僅對L1存儲器中的數據進行讀寫。因而,內核不須要等待數據的到來,由於DMA引擎會在內核準備讀取數據以前將數據準備好。圖2給出了處理器和DMA控制器間的交互關係。由處理器完成的操做步驟包括:創建傳輸,啓用中斷,生成中斷時執行代碼。返回處處理器的中斷輸入能夠用來指示「數據已經準備好,可進行處理」。

圖2:DMA控制器.
圖2:DMA控制器。

數據除了往來外設以外,還須要從一個存儲器空間轉移到另外一個空間中。例如,視頻源能夠從一個視頻端口直接流入L3存儲器,由於工做緩衝區規模太大,沒法放入到存儲器中。咱們並不但願讓處理器在每次須要執行計算時都從外部存儲讀取像素信息,所以爲了提升存取的效率,能夠用一個存儲器到存儲器的DMA(MemDMA)來將像素轉移到L1或者L2存儲器中。

到目前爲之,咱們還僅專一於數據的移動,可是DMA的傳送能力並不老是用來移動數據。  

 在最簡單的MemDMA狀況中,咱們須要告訴DMA控制器源端地址、目標端地址和待傳送的字的個數。每次傳輸的字的大小能夠是八、16或者12位。  咱們只須要改變數據傳輸每次的數據大小,就能夠簡單地增長DMA的靈活性。例如,採用非單一大小的傳輸方式時,咱們以傳輸數據塊的大小的倍數來做爲地址增量。也就是說,若規定32位的傳輸和4個採樣的跨度,則每次傳輸結束後,地址的增量爲16字節(4個32位字)。

 

DMA的設置

目前有兩類主要的DMA傳輸結構:寄存器模式和描述符模式。不管屬於哪一類DMA,表1所描述的幾類信息都會在DMA控制器中出現。當DMA以寄存器模式工做時,DMA控制器只是簡單地利用寄存器中所存儲的參數值。在描述符模式中,DMA控制器在存儲器中查找本身的配置參數。

表1:DMA寄存器
表1:DMA寄存器

基於寄存器的DMA

在基於寄存器的DMA內部,處理器直接對DMA控制寄存器進行編程,來啓動傳輸。基於寄存器的DMA提供了最佳的DMA控制器性能,由於寄存器並不須要不斷地從存儲器中的描述符上載入數據,而內核也不須要保持描述符。

基於寄存器的DMA由兩種子模式組成:自動緩衝(Autobuffer)模式和中止模式。在自動緩衝DMA中,當一個傳輸塊傳輸完畢,控制寄存器就自動從新載入其最初的設定值,同一個DMA進程從新啓動,開銷爲零。

正如咱們在圖3中所看到的那樣,若是將一個自動緩衝DMA設定爲從外設傳輸必定數量的字到L1數據存儲器的緩衝器上,則DMA控制器將會在最後一個字傳輸完成的時刻就迅速從新載入初始的參數。這構成了一個「循環緩衝器」,由於當一個量值被寫入到緩衝器的最後一個位置上時,下一個值將被寫入到緩衝器的第一個位置上。

圖3:用DMA實現循環緩衝器.
圖3:用DMA實現循環緩衝器。

自動緩衝DMA特別適合於對性能敏感的、存在持續數據流的應用。DMA控制器能夠在獨立於處理器其餘活動的狀況下讀入數據流,而後在每次傳輸結束時,向內核發出中斷。

中止模式的工做方式與自動緩衝DMA相似,區別在於各寄存器在DMA結束後不會從新載入,所以整個DMA傳輸只發生一次。中止模式對於基於某種事件的一次性傳輸來講十分有用。例如,非按期地將數據塊從一個位置轉移到另外一個位置。當你須要對事件進行同步時,這種模式也很是有用。例如,若是一個任務必須在下一次傳輸前完成的話,則中止模式能夠確保各事件發生的前後順序。此外,中止模式對於緩衝器的初始化來講很是有用。

描述符模型

基於描述符(descriptor)的DMA要求在存儲器中存入一組參數,以啓動DMA的系列操做。該描述符所包含的參數與那些一般經過編程寫入DMA控制寄存器組的全部參數相同。不過,描述符還能夠允許多個DMA操做序列串在一塊兒。在基於描述符的DMA操做中,咱們能夠對一個DMA通道進行編程,在當前的操做序列完成後,自動設置並啓動另外一次DMA傳輸。基於描述符的方式爲管理系統中的DMA傳輸提供了最大的靈活性。

ADI 的Blackfin處理器上有兩種主要的描述符方式—描述符陣列和描述符列表,這兩種操做方式所要實現的目標是在靈活性和性能之間實現一種折中平衡。

 DMA 方式, 即外設在專用的接口電路DMA 控制器的控制下直接和存儲器進行高速數據傳送。採用DMA 方式時,如外設
須要進行數據傳輸, 首先向DMA 控制器發出請求,DMA 再向CPU 發出總線請求,要求控制系統總線。CPU 響應DMA 控制器
的總線請求並把總線控制權交給DMA, 而後在DMA 的控制下開始利用系統總線進行數據傳輸。數據傳輸結束後,DMA 並回
總線控制權。DMA 操做步驟:
(1) DMA 控制器的初始化
(2) DMA 數據傳送
(3) DMA 結束
DMA 初始化預置以下信息:一是指定I/O 設備對外設"讀"仍是"寫",即指定其控制/狀態寄存器中相應的控制位;二是數據應傳送至何處,指定其地址的首地址;三是有多少數據字須要傳送。

 

 

 

DMA原理解析

DMA概念

DMA(Direct Memory Access,直接內存存取) ,DMA 傳輸將數據從一個地址空間複製到另一個地址空間。採用CPU來初始化這個傳輸動做,可是傳輸動做自己是由 DMA 控制器來實行和完成,不須要佔用CPU。

DMA控制器(以2440爲例)

2440芯片手冊第8章爲DMA控制器。
2440的DMA控制器支持4個通道

請求源:
這裏寫圖片描述
上圖是2440中DMA控制器支持的請求源。

基本時序
這裏寫圖片描述
在請求信號有效以後,通過2個週期DACK信號有效,再通過3個週期,DMA控制器纔可得到總線的控制權,開始讀寫操做。

工做模式
Demond模式:
若是DMA完成一次請求後若是Request仍然有效,那麼DMA就認爲這是下一次DMA請求,並當即開始下一次的傳輸。

Handshake模式:
DMA完成一次請求後等待Request信號無效,若是Request 無效,DMA會無效ACK兩個時鐘週期,再等待下一次Request。

6410芯片的DMA控制器在芯片手冊的第11章。

DMA程序設計(2440芯片)

char *buf = "Hello World!"; #define DISRC0 (*(volatile unsigned long*)0x4B000000) #define DISRCC0 (*(volatile unsigned long*)0x4B000004) #define DIDST0 (*(volatile unsigned long*)0x4B000008) #define DIDSTC0 (*(volatile unsigned long*)0x4B00000C) #define DCON0 (*(volatile unsigned long*)0x4B000010) #define DMASKTRIG0 (*(volatile unsigned long*)0x4B000020) #define UTXH0 (volatile unsigned long*)0x50000020 void dma_init() { //初始化源地址 DISRC0 = (unsigned int)buf; //向寄存器中填寫源地址 DISRCC0 = (0<<1)| (0<<0); //內存使用的是AHB總線,源地址須要增加 //初始化目的地址 DIDST0 = UTXH0; //向串口中傳送數據 DIDSTC0 = (1<<1)| (1<<0); //串口使用的是APB總線,目的地址老是一個寄存器不增加 DCON0 = (1<<24)| (1<<23)| (1<<22)| (12<<0); //控制寄存器,選擇DMA源,硬件,是否屢次發送,數據個數 } void dma_start() { DMASKTRIG0 = (1<<1);//啓動傳輸 }
  • 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

DMA程序設計(6410芯片)

/* S3C6410中DMA操做步驟: 一、決定使用安全DMAC(SDMAC)仍是通用DMAC(DMAC); 二、開啓DMAC控制,設置DMAC_Configuration寄存器; 三、清除傳輸結束中斷寄存器和錯誤中斷寄存器; 四、選擇合適的優先級通道; 五、設置通道的源數據地址和目的數據地址(設置DMACC_SrcAddr和DMACC_DestAddr); 六、設置通道控制寄存器0(設置DMACC_Control0); 七、設置通道控制寄存器1,(傳輸大小,設置DMACC_Control1); 八、設置通道配置寄存器;(設置DMACC_Configuration) 九、使能相應通道(設置DMACC_Configuratoin); */ #define SDMA_SEL (*((volatile unsigned long *)0x7E00F110)) #define DMACIntTCClear (*((volatile unsigned long *)0x7DB00008)) #define DMACIntErrClr (*((volatile unsigned long *)0x7DB00010)) #define DMACConfiguration (*((volatile unsigned long *)0x7DB00030)) #define DMACSync (*((volatile unsigned long *)0x7DB00034)) #define DMACC0SrcAddr (*((volatile unsigned long *)0x7DB00100)) #define DMACC0DestAddr (*((volatile unsigned long *)0x7DB00104)) #define DMACC0Control0 (*((volatile unsigned long *)0x7DB0010c)) #define DMACC0Control1 (*((volatile unsigned long *)0x7DB00110)) #define DMACC0Configuration (*((volatile unsigned long *)0x7DB00114)) #define UTXH0 (volatile unsigned long *)0x7F005020 char src[100] = "\n\rHello World-> This is a test!\n\r"; void dma_init() { //DMA控制器的選擇(SDMAC0) SDMA_SEL = 0; //DMA控制器使能 DMACConfiguration = 1; //初始化源地址 DMACC0SrcAddr = (unsigned int)src; //初始化目的地址 DMACC0DestAddr = (unsigned int)UTXH0; //對控制寄存器進行配置 /* 源地址自增 目的地址固定、 目標主機選擇AHB主機2 源主機選擇AHB主機1 */ DMACC0Control0 =(1<<25) | (1 << 26)| (1<<31); DMACC0Control1 = 0x64; //傳輸的大小 /* 流控制和傳輸類型:MTP 爲 001 目標外設:DMA_UART0_1,源外設:DMA_MEM 通道有效: 1 */ DMACC0Configuration = (1<<6) | (1<<11) | (1<<14) | (1<<15); } void dma_start() { //開啓channel0 DMA DMACC0Configuration = 1; }

 

 

 

Linux內核DMA機制

DMA控制器硬件結構

DMA容許外圍設備和主內存之間直接傳輸 I/O 數據, DMA 依賴於系統。每一種體系結構DMA傳輸不一樣,編程接口也不一樣。

數據傳輸能夠以兩種方式觸發:一種軟件請求數據,另外一種由硬件異步傳輸。

在第一種狀況下,調用的步驟能夠歸納以下(以read爲例):

(1)在進程調用 read 時,驅動程序的方法分配一個 DMA 緩衝區,隨後指示硬件傳送它的數據。進程進入睡眠。

(2)硬件將數據寫入 DMA 緩衝區並在完成時產生一箇中斷。

(3)中斷處理程序得到輸入數據,應答中斷,最後喚醒進程,該進程如今能夠讀取數據了。

第二種情形是在 DMA 被異步使用時發生的。以數據採集設備爲例:

(1)硬件發出中斷來通知新的數據已經到達。

(2)中斷處理程序分配一個DMA緩衝區。

(3)外圍設備將數據寫入緩衝區,而後在完成時發出另外一箇中斷。

(4)處理程序利用DMA分發新的數據,喚醒任何相關進程。

網卡傳輸也是如此,網卡有一個循環緩衝區(一般叫作 DMA 環形緩衝區)創建在與處理器共享的內存中。每個輸入數據包被放置在環形緩衝區中下一個可用緩衝區,而且發出中斷。而後驅動程序將網絡數據包傳給內核的其它部分處理,並在環形緩衝區中放置一個新的 DMA 緩衝區。

驅動程序在初始化時分配DMA緩衝區,並使用它們直到中止運行。

DMA控制器依賴於平臺硬件,這裏只對i386的8237 DMA控制器作簡單的說明,它有兩個控制器,8個通道,具體說明以下:

控制器1: 通道0-3,字節操做, 端口爲 00-1F

控制器2: 通道 4-7, 字操做, 端口咪 C0-DF

- 全部寄存器是8 bit,與傳輸大小無關。

  - 通道 4 被用來將控制器1與控制器2級聯起來。

- 通道 0-3 是字節操做,地址/計數都是字節的。

- 通道 5-7 是字操做,地址/計數都是以字爲單位的。

- 傳輸器對於(0-3通道)必須不超過64K的物理邊界,對於5-7必須不超過128K邊界。

- 對於5-7通道page registers 不用數據 bit 0, 表明128K頁

- 對於0-3通道page registers 使用 bit 0, 表示 64K頁

DMA 傳輸器限制在低於16M物理內存裏。裝入寄存器的地址必須是物理地址,而不是邏輯地址。

 對於0-3通道來講地址對寄存器的映射以下:

A23 ... A16 A15 ... A8  A7 ... A0    (物理地址) | ... | | ... | | ... | | ... | | ... | | ... | | ... | | ... | | ... | P7 ... P0 A7 ... A0 A7 ... A0 | Page | Addr MSB | Addr LSB | (DMA 地址寄存器)


對於5-7通道來講地址對寄存器的映射以下:

A23 ... A17 A16 A15 ... A9 A8 A7 ... A1 A0    (物理地址) 
    |  ...  | \ \ ... \ \ \ ... \ \ | ... | \ \ ... \ \ \ ... \ (沒用) | ... | \ \ ... \ \ \ ... \ P7 ... P1 (0) A7 A6 ... A0 A7 A6 ... A0 
| Page | Addr MSB | Addr LSB | (DMA 地址寄存器)


通道 5-7 傳輸以字爲單位, 地址和計數都必須是以字對齊的。

在include/asm-i386/dma.h中有i386平臺的8237 DMA控制器的各處寄存器的地址及寄存器的定義,這裏只對控制寄存器加以說明:

DMA Channel Control/Status Register (DCSRX)

第31位 代表是否開始

第30位 選定Descriptor和Non-Descriptor模式

第29位 判斷有無中斷

第8位 請求處理 (Request Pending)

第3位 Channel是否運行

第2位 當前數據交換是否完成

第1位 是否由Descriptor產生中斷

第0位 是否由總線錯誤引發中斷

DMA通道使用的地址

DMA通道用dma_chan結構數組表示,這個結構在kernel/dma.c中,列出以下:
struct dma_chan { int lock; const char *device_id; }; static struct dma_chan dma_chan_busy[MAX_DMA_CHANNELS] = { [4] = { 1, "cascade" }, };

若是dma_chan_busy[n].lock != 0表示忙,DMA0保留爲DRAM更新用,DMA4用做級聯。DMA 緩衝區的主要問題是,當它大於一頁時,它必須佔據物理內存中的連續頁。

因爲DMA須要連續的內存,於是在引導時分配內存或者爲緩衝區保留物理 RAM 的頂部。在引導時給內核傳遞一個"mem="參數能夠保留 RAM 的頂部。例如,若是系統有 32MB 內存,參數"mem=31M"阻止內核使用最頂部的一兆字節。稍後,模塊能夠使用下面的代碼來訪問這些保留的內存:

dmabuf = ioremap( 0x1F00000 /* 31M */, 0x100000 /* 1M */);

分配 DMA 空間的方法,代碼調用 kmalloc(GFP_ATOMIC) 直到失敗爲止,而後它等待內核釋放若干頁面,接下來再一次進行分配。最終會發現由連續頁面組成的DMA 緩衝區的出現。

一個使用 DMA 的設備驅動程序一般會與鏈接到接口總線上的硬件通信,這些硬件使用物理地址,而程序代碼使用虛擬地址。基於 DMA 的硬件使用總線地址而不是物理地址,有時,接口總線是經過將 I/O 地址映射到不一樣物理地址的橋接電路鏈接的。甚至某些系統有一個頁面映射方案,可以使任意頁面在外圍總線上表現爲連續的。

當驅動程序須要向一個 I/O 設備(例如擴展板或者DMA控制器)發送地址信息時,必須使用 virt_to_bus 轉換,在接受到來自鏈接到總線上硬件的地址信息時,必須使用 bus_to_virt 了。

DMA操做函數

由於 DMA 控制器是一個系統級的資源,因此內核協助處理這一資源。內核使用 DMA 註冊表爲 DMA 通道提供了請求/釋放機制,而且提供了一組函數在 DMA 控制器中配置通道信息。

DMA 控制器使用函數request_dma和free_dma來獲取和釋放 DMA 通道的全部權,請求 DMA 通道應在請求了中斷線以後,而且在釋放中斷線以前釋放它。每個使用 DMA 的設備也必須使用中斷信號線,不然就沒法發出數據傳輸完成的通知。這兩個函數的聲明列出以下(在kernel/dma.c中):
int request_dma(unsigned int channel, const char *name); void free_dma(unsigned int channel);

DMA 控制器被dma_spin_lock 的自旋鎖所保護。使用函數claim_dma_lock和release_dma_lock對得到和釋放自旋鎖。這兩個函數的聲明列出以下(在kernel/dma.c中):

unsigned long claim_dma_lock(); 獲取 DMA 自旋鎖,該函數會阻塞本地處理器上的中斷,所以,其返回值是"標誌"值,在從新打開中斷時必須使用該值。

void release_dma_lock(unsigned long flags); 釋放 DMA 自旋鎖,而且恢復之前的中斷狀態。

DMA 控制器的控制設置信息由RAM 地址、傳輸的數據(以字節或字爲單位),以及傳輸的方向三部分組成。下面是i386平臺的8237 DMA控制器的操做函數說明(在include/asm-i386/dma.h中),使用這些函數設置DMA控制器時,應該持有自旋鎖。但在驅動程序作I/O 操做時,不能持有自旋鎖。

void set_dma_mode(unsigned int channel, char mode); 該函數指出通道從設備讀(DMA_MODE_WRITE)或寫(DMA_MODE_READ)數據方式,當mode設置爲 DMA_MODE_CASCADE時,表示釋放對總線的控制。

void set_dma_addr(unsigned int channel, unsigned int addr); 函數給 DMA 緩衝區的地址賦值。該函數將 addr 的最低 24 位存儲到控制器中。參數 addr 是總線地址。

void set_dma_count(unsigned int channel, unsigned int count);該函數對傳輸的字節數賦值。參數 count 也表明 16 位通道的字節數,在此狀況下,這個數字必須是偶數。

除了這些操做函數外,還有些對DMA狀態進行控制的工具函數:

void disable_dma(unsigned int channel); 該函數設置禁止使用DMA 通道。這應該在配置 DMA 控制器以前設置。

void enable_dma(unsigned int channel); 在DMA 通道中包含了合法的數據時,該函數激活DMA 控制器。

int get_dma_residue(unsigned int channel); 該函數查詢一個 DMA 傳輸還有多少字節還沒傳輸完。函數返回沒傳完的字節數。當傳輸成功時,函數返回值是0。

void clear_dma_ff(unsigned int channel) 該函數清除 DMA 觸發器(flip-flop),該觸發器用來控制對 16 位寄存器的訪問。能夠經過兩個連續的 8 位操做來訪問這些寄存器,觸發器被清除時用來選擇低字節,觸發器被置位時用來選擇高字節。在傳輸 8 位後,觸發器會自動反轉;在訪問 DMA 寄存器以前,程序員必須清除觸發器(將它設置爲某個已知狀態)。

DMA映射

一個DMA映射就是分配一個 DMA 緩衝區併爲該緩衝區生成一個可以被設備訪問的地址的組合操做。通常狀況下,簡單地調用函數virt_to_bus 就設備總線上的地址,但有些硬件映射寄存器也被設置在總線硬件中。映射寄存器(mapping register)是一個相似於外圍設備的虛擬內存等價物。在使用這些寄存器的系統上,外圍設備有一個相對較小的、專用的地址區段,能夠在此區段執行 DMA。經過映射寄存器,這些地址被重映射到系統 RAM。映射寄存器具備一些好的特性,包括使分散的頁面在設備地址空間看起來是連續的。但不是全部的體系結構都有映射寄存器,特別地,PC 平臺沒有映射寄存器。

在某些狀況下,爲設備設置有用的地址也意味着須要構造一個反彈(bounce)緩衝區。例如,當驅動程序試圖在一個不能被外圍設備訪問的地址(一個高端內存地址)上執行 DMA 時,反彈緩衝區被建立。而後,按照須要,數據被複制到反彈緩衝區,或者從反彈緩衝區複製。

根據 DMA 緩衝區指望保留的時間長短,PCI 代碼區分兩種類型的 DMA 映射:

  • 一致 DMA 映射 它們存在於驅動程序的生命週期內。一個被一致映射的緩衝區必須同時可被 CPU 和外圍設備訪問,這個緩衝區被處理器寫時,可當即被設備讀取而沒有cache效應,反之亦然,使用函數pci_alloc_consistent創建一致映射。
  • 流式 DMA映射 流式DMA映射是爲單個操做進行的設置。它映射處理器虛擬空間的一塊地址,以至它能被設備訪問。應儘量使用流式映射,而不是一致映射。這是由於在支持一致映射的系統上,每一個 DMA 映射會使用總線上一個或多個映射寄存器。具備較長生命週期的一致映射,會獨佔這些寄存器很長時間――即便它們沒有被使用。使用函數dma_map_single創建流式映射。

(1)創建一致 DMA 映射

函數pci_alloc_consistent處理緩衝區的分配和映射,函數分析以下(在include/asm-generic/pci-dma-compat.h中):
static inline void *pci_alloc_consistent(struct pci_dev *hwdev,                  size_t size, dma_addr_t *dma_handle) { return dma_alloc_coherent(hwdev == NULL ? NULL : &hwdev->dev, size, dma_handle, GFP_ATOMIC); }

結構dma_coherent_mem定義了DMA一致性映射的內存的地址、大小和標識等。結構dma_coherent_mem列出以下(在arch/i386/kernel/pci-dma.c中):
struct dma_coherent_mem { 

void *virt_base; u32 device_base; int size; int flags; unsigned long *bitmap;

};

函數dma_alloc_coherent分配size字節的區域的一致內存,獲得的dma_handle是指向分配的區域的地址指針,這個地址做爲區域的物理基地址。dma_handle是與總線同樣的位寬的無符號整數。 函數dma_alloc_coherent分析以下(在arch/i386/kernel/pci-dma.c中):
void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, int gfp) { void *ret;   //如果設備,獲得設備的dma內存區域,即mem= dev->dma_mem struct dma_coherent_mem *mem = dev ? dev->dma_mem : NULL; int order = get_order(size);//將size轉換成order,即 //忽略特定的區域,於是忽略這兩個標識 gfp &= ~(__GFP_DMA | __GFP_HIGHMEM); if (mem) {//設備的DMA映射,mem= dev->dma_mem     //找到mem對應的頁 int page = bitmap_find_free_region(mem->bitmap, mem->size, order); if (page >= 0) { *dma_handle = mem->device_base + (page << PAGE_SHIFT); ret = mem->virt_base + (page << PAGE_SHIFT); memset(ret, 0, size); return ret; } if (mem->flags & DMA_MEMORY_EXCLUSIVE) return NULL; }   //不是設備的DMA映射 if (dev == NULL || (dev->coherent_dma_mask < 0xffffffff)) gfp |= GFP_DMA;   //分配空閒頁 ret = (void *)__get_free_pages(gfp, order); if (ret != NULL) { memset(ret, 0, size);//清0 *dma_handle = virt_to_phys(ret);//獲得物理地址 } return ret; }

當再也不須要緩衝區時(一般在模塊卸載時),應該調用函數 pci_free_consitent 將它返還給系統。

(2)創建流式 DMA 映射

在流式 DMA 映射的操做中,緩衝區傳送方向應匹配於映射時給定的方向值。緩衝區被映射後,它就屬於設備而再也不屬於處理器了。在緩衝區調用函數pci_unmap_single撤銷映射以前,驅動程序不該該觸及其內容。

在緩衝區爲 DMA 映射時,內核必須確保緩衝區中全部的數據已經被實際寫到內存。可能有些數據還會保留在處理器的高速緩衝存儲器中,所以必須顯式刷新。在刷新以後,由處理器寫入緩衝區的數據對設備來講也許是不可見的。

若是欲映射的緩衝區位於設備不能訪問的內存區段時,某些體系結構僅僅會操做失敗,而其它的體系結構會建立一個反彈緩衝區。反彈緩衝區是被設備訪問的獨立內存區域,反彈緩衝區複製原始緩衝區的內容。

函數pci_map_single映射單個用於傳送的緩衝區,返回值是能夠傳遞給設備的總線地址,若是出錯的話就爲 NULL。一旦傳送完成,應該使用函數pci_unmap_single 刪除映射。其中,參數direction爲傳輸的方向,取值以下:

PCI_DMA_TODEVICE 數據被髮送到設備。

PCI_DMA_FROMDEVICE若是數據將發送到 CPU。

PCI_DMA_BIDIRECTIONAL數據進行兩個方向的移動。

PCI_DMA_NONE 這個符號只是爲幫助調試而提供。

函數pci_map_single分析以下(在arch/i386/kernel/pci-dma.c中):
static inline dma_addr_t pci_map_single(struct pci_dev *hwdev,                 void *ptr, size_t size, int direction) { return dma_map_single(hwdev == NULL ? NULL : &hwdev->dev, ptr, size, (enum ma_data_direction)direction); }

函數dma_map_single映射一塊處理器虛擬內存,這塊虛擬內存能被設備訪問,返回內存的物理地址,函數dma_map_single分析以下(在include/asm-i386/dma-mapping.h中):
static inline dma_addr_t dma_map_single(struct device *dev, void *ptr, 
                        size_t size, enum dma_data_direction direction) 

{BUG_ON(direction == DMA_NONE);  //可能有些數據還會保留在處理器的高速緩衝存儲器中,所以必須顯式刷新flush_write_buffers();return virt_to_phys(ptr); //虛擬地址轉化爲物理地址

}

(3)分散/集中映射

分散/集中映射是流式 DMA 映射的一個特例。它將幾個緩衝區集中到一塊兒進行一次映射,並在一個 DMA 操做中傳送全部數據。這些分散的緩衝區由分散表結構scatterlist來描述,多個分散的緩衝區的分散表結構組成緩衝區的struct scatterlist數組。

分散表結構列出以下(在include/asm-i386/scatterlist.h):
struct scatterlist { struct page *page; unsigned int offset; dma_addr_t dma_address; //用在分散/集中操做中的緩衝區地址 unsigned int length;//該緩衝區的長度 };

每個緩衝區的地址和長度會被存儲在 struct scatterlist 項中,但在不一樣的體系結構中它們在結構中的位置是不一樣的。下面的兩個宏定義來解決平臺移植性問題,這些宏定義應該在一個pci_map_sg 被調用後使用:
//從該分散表項中返回總線地址

#define sg_dma_address(sg) �sg)->dma_address) //返回該緩衝區的長度 

#define sg_dma_len(sg) �sg)->length)

函數pci_map_sg完成分散/集中映射,其返回值是要傳送的 DMA 緩衝區數;它可能會小於 nents(也就是傳入的分散表項的數量),由於可能有的緩衝區地址上是相鄰的。一旦傳輸完成,分散/集中映射經過調用函數pci_unmap_sg 來撤銷映射。 函數pci_map_sg分析以下(在include/asm-generic/pci-dma-compat.h中):
static inline int pci_map_sg(struct pci_dev *hwdev, struct scatterlist *sg, int nents, int direction) { return dma_map_sg(hwdev == NULL ? NULL : &hwdev->dev, sg, nents, (enum dma_data_direction)direction); } include/asm-i386/dma-mapping.h static inline int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction) { int i; BUG_ON(direction == DMA_NONE); for (i = 0; i < nents; i++ ) { BUG_ON(!sg[i].page);     //將頁及頁偏移地址轉化爲物理地址 sg[i].dma_address = page_to_phys(sg[i].page) + sg[i].offset; } //可能有些數據還會保留在處理器的高速緩衝存儲器中,所以必須顯式刷新 flush_write_buffers(); return nents; }

DMA池

許多驅動程序須要又多又小的一致映射內存區域給DMA描述子或I/O緩存buffer,這使用DMA池比用dma_alloc_coherent分配的一頁或多頁內存區域好,DMA池用函數dma_pool_create建立,用函數dma_pool_alloc從DMA池中分配一塊一致內存,用函數dmp_pool_free放內存回到DMA池中,使用函數dma_pool_destory釋放DMA池的資源。

結構dma_pool是DMA池描述結構,列出以下:
struct dma_pool { /* the pool */ struct list_head page_list;//頁鏈表 spinlock_t lock; size_t blocks_per_page; //每頁的塊數 size_t size; //DMA池裏的一致內存塊的大小 struct device *dev; //將作DMA的設備 size_t allocation; //分配的沒有跨越邊界的塊數,是size的整數倍 char name [32]; //池的名字 wait_queue_head_t waitq; //等待隊列 struct list_head pools; };

函數dma_pool_create給DMA建立一個一致內存塊池,其參數name是DMA池的名字,用於診斷用,參數dev是將作DMA的設備,參數size是DMA池裏的塊的大小,參數align是塊的對齊要求,是2的冪,參數allocation返回沒有跨越邊界的塊數(或0)。

函數dma_pool_create返回建立的帶有要求字符串的DMA池,若建立失敗返回null。對被給的DMA池,函數dma_pool_alloc被用來分配內存,這些內存都是一致DMA映射,可被設備訪問,且沒有使用緩存刷新機制,由於對齊緣由,分配的塊的實際尺寸比請求的大。若是分配非0的內存,從函數dma_pool_alloc返回的對象將不跨越size邊界(如不跨越4K字節邊界)。這對在個體的DMA傳輸上有地址限制的設備來講是有利的。

  函數dma_pool_create分析以下(在drivers/base/dmapool.c中):
struct dma_pool *dma_pool_create (const char *name, struct device *dev,           size_t size, size_t align, size_t allocation) { struct dma_pool *retval; if (align == 0) align = 1; if (size == 0) return NULL; else if (size < align) size = align; else if ((size % align) != 0) {//對齊處理 size += align + 1; size &= ~(align - 1); }   //若是一致內存塊比頁大,是分配爲一致內存塊大小,不然,分配爲頁大小 if (allocation == 0) { if (PAGE_SIZE < size)//頁比一致內存塊小 allocation = size; else allocation = PAGE_SIZE;//頁大小 // FIXME: round up for less fragmentation } else if (allocation < size) return NULL;   //分配dma_pool結構對象空間 if (!(retval = kmalloc (sizeof *retval, SLAB_KERNEL))) return retval; strlcpy (retval->name, name, sizeof retval->name); retval->dev = dev;   //初始化dma_pool結構對象retval INIT_LIST_HEAD (&retval->page_list);//初始化頁鏈表 spin_lock_init (&retval->lock); retval->size = size; retval->allocation = allocation; retval->blocks_per_page = allocation / size; init_waitqueue_head (&retval->waitq);//初始化等待隊列 if (dev) {//設備存在時 down (&pools_lock); if (list_empty (&dev->dma_pools))       //給設備建立sysfs文件系統屬性文件 device_create_file (dev, &dev_attr_pools); /* note: not currently insisting "name" be unique */ list_add (&retval->pools, &dev->dma_pools); //將DMA池加到dev中 up (&pools_lock); } else INIT_LIST_HEAD (&retval->pools); return retval; }

函數dma_pool_alloc從DMA池中分配一塊一致內存,其參數pool是將產生塊的DMA池,參數mem_flags是GFP_*位掩碼,參數handle是指向塊的DMA地址,函數dma_pool_alloc返回當前沒用的塊的內核虛擬地址,並經過handle給出它的DMA地址,若是內存塊不能被分配,返回null。

函數dma_pool_alloc包裹了dma_alloc_coherent頁分配器,這樣小塊更容易被總線的主控制器使用。這可能共享slab分配器的內容。

函數dma_pool_alloc分析以下(在drivers/base/dmapool.c中):
void *dma_pool_alloc (struct dma_pool *pool, int mem_flags, dma_addr_t *handle) { unsigned long flags; struct dma_page *page; int map, block; size_t offset; void *retval; restart: spin_lock_irqsave (&pool->lock, flags); list_for_each_entry(page, &pool->page_list, page_list) { int i; /* only cachable accesses here ... */ //遍歷一頁的每塊,而每塊又以32字節遞增 for (map = 0, i = 0; i < pool->blocks_per_page; //每頁的塊數 i += BITS_PER_LONG, map++) { // BITS_PER_LONG定義爲32 if (page->bitmap [map] == 0) continue; block = ffz (~ page->bitmap [map]);//找出第一個0 if ((i + block) < pool->blocks_per_page) { clear_bit (block, &page->bitmap [map]);        //獲得相對於頁邊界的偏移 offset = (BITS_PER_LONG * map) + block; offset *= pool->size; goto ready; } } } //給DMA池分配dma_page結構空間,加入到pool->page_list鏈表, //並做DMA一致映射,它包括分配給DMA池一頁。 // SLAB_ATOMIC表示調用 kmalloc(GFP_ATOMIC) 直到失敗爲止, //而後它等待內核釋放若干頁面,接下來再一次進行分配。 if (!(page = pool_alloc_page (pool, SLAB_ATOMIC))) { if (mem_flags & __GFP_WAIT) { DECLARE_WAITQUEUE (wait, current); current->state = TASK_INTERRUPTIBLE; add_wait_queue (&pool->waitq, &wait); spin_unlock_irqrestore (&pool->lock, flags); schedule_timeout (POOL_TIMEOUT_JIFFIES); remove_wait_queue (&pool->waitq, &wait); goto restart; } retval = NULL; goto done; } clear_bit (0, &page->bitmap [0]); offset = 0; ready: page->in_use++; retval = offset + page->vaddr; //返回虛擬地址 *handle = offset + page->dma; //相對DMA地址 #ifdef CONFIG_DEBUG_SLAB memset (retval, POOL_POISON_ALLOCATED, pool->size); #endif done: spin_unlock_irqrestore (&pool->lock, flags); return retval; }

一個簡單的使用DMA 例子

示例:下面是一個簡單的使用DMA進行傳輸的驅動程序,它是一個假想的設備,只列出DMA相關的部分來講明驅動程序中如何使用DMA的。

函數dad_transfer是設置DMA對內存buffer的傳輸操做函數,它使用流式映射將buffer的虛擬地址轉換到物理地址,設置好DMA控制器,而後開始傳輸數據。
int dad_transfer(struct dad_dev *dev, int write, void *buffer, size_t count) { dma_addr_t bus_addr; unsigned long flags; /* Map the buffer for DMA */ dev->dma_dir = (write ? PCI_DMA_TODEVICE : PCI_DMA_FROMDEVICE); dev->dma_size = count;   //流式映射,將buffer的虛擬地址轉化成物理地址 bus_addr = pci_map_single(dev->pci_dev, buffer, count, dev->dma_dir); dev->dma_addr = bus_addr; //DMA傳送的buffer物理地址 //將操做控制寫入到DMA控制器寄存器,從而創建起設備 writeb(dev->registers.command, DAD_CMD_DISABLEDMA);  //設置傳輸方向--讀仍是寫 writeb(dev->registers.command, write ? DAD_CMD_WR : DAD_CMD_RD);  writel(dev->registers.addr, cpu_to_le32(bus_addr));//buffer物理地址 writel(dev->registers.len, cpu_to_le32(count)); //傳輸的字節數 //開始激活DMA進行數據傳輸操做 writeb(dev->registers.command, DAD_CMD_ENABLEDMA); return 0; }

函數dad_interrupt是中斷處理函數,當DMA傳輸完時,調用這個中斷函數來取消buffer上的DMA映射,從而讓內核程序能夠訪問這個buffer。
void dad_interrupt(int irq, void *dev_id, struct pt_regs *regs) 

{

  struct dad_dev *dev = (struct dad_dev *) dev_id; 
  /* Make sure it's really our device interrupting */ 
  /* Unmap the DMA buffer */ 
  pci_unmap_single(dev->pci_dev, dev->dma_addr, dev->dma_size, dev->dma_dir); 
  /* Only now is it safe to access the buffer, copy to user, etc. */ 
  ... 
}

函數dad_open打開設備,此時應申請中斷號及DMA通道。
int dad_open (struct inode *inode, struct file *filp) 

{

  struct dad_device *my_device; 
  // SA_INTERRUPT表示快速中斷處理且不支持共享 IRQ 信號線
  if ( (error = request_irq(my_device.irq, dad_interrupt, SA_INTERRUPT, "dad", NULL)) ) return error; /* or implement blocking open */ 
  if ( (error = request_dma(my_device.dma, "dad")) ) { free_irq(my_device.irq, NULL); return error; /* or implement blocking open */ } 
  return 0; 
}

在與open 相對應的 close 函數中應該釋放DMA及中斷號。
void dad_close (struct inode *inode, struct file *filp) 

{

  struct dad_device *my_device; free_dma(my_device.dma); free_irq(my_device.irq, NULL); …… 
}

函數dad_dma_prepare初始化DMA控制器,設置DMA控制器的寄存器的值,爲 DMA 傳輸做準備。
int dad_dma_prepare(int channel, int mode, unsigned int buf, 
                  unsigned int count) 

{

  unsigned long flags; 
  flags = claim_dma_lock(); disable_dma(channel); clear_dma_ff(channel); set_dma_mode(channel, mode); set_dma_addr(channel, virt_to_bus(buf)); set_dma_count(channel, count); enable_dma(channel); release_dma_lock(flags); 
  return 0; 
}

函數dad_dma_isdone用來檢查 DMA 傳輸是否成功結束。
int dad_dma_isdone(int channel) { int residue; unsigned long flags = claim_dma_lock (); residue = get_dma_residue(channel); release_dma_lock(flags); return (residue == 0); }

 

 

 

Linux 內核DMA機制

DMA控制器硬件結構

DMA容許外圍設備和主內存之間直接傳輸 I/O 數據, DMA 依賴於系統。每一種體系結構DMA傳輸不一樣,編程接口也不一樣。

數據傳輸能夠以兩種方式觸發:一種軟件請求數據,另外一種由硬件異步傳輸。

在第一種狀況下,調用的步驟能夠歸納以下(以read爲例):

(1)在進程調用 read 時,驅動程序的方法分配一個 DMA 緩衝區,隨後指示硬件傳送它的數據。進程進入睡眠。

(2)硬件將數據寫入 DMA 緩衝區並在完成時產生一箇中斷。

(3)中斷處理程序得到輸入數據,應答中斷,最後喚醒進程,該進程如今能夠讀取數據了。

第二種情形是在 DMA 被異步使用時發生的。以數據採集設備爲例:

(1)硬件發出中斷來通知新的數據已經到達。

(2)中斷處理程序分配一個DMA緩衝區。

(3)外圍設備將數據寫入緩衝區,而後在完成時發出另外一箇中斷。

(4)處理程序利用DMA分發新的數據,喚醒任何相關進程。

網卡傳輸也是如此,網卡有一個循環緩衝區(一般叫作 DMA 環形緩衝區)創建在與處理器共享的內存中。每個輸入數據包被放置在環形緩衝區中下一個可用緩衝區,而且發出中斷。而後驅動程序將網絡數據包傳給內核的其它部分處理,並在環形緩衝區中放置一個新的 DMA 緩衝區。

驅動程序在初始化時分配DMA緩衝區,並使用它們直到中止運行。

DMA控制器依賴於平臺硬件,這裏只對i386的8237 DMA控制器作簡單的說明,它有兩個控制器,8個通道,具體說明以下:

控制器1: 通道0-3,字節操做, 端口爲 00-1F

控制器2: 通道 4-7, 字操做, 端口咪 C0-DF

- 全部寄存器是8 bit,與傳輸大小無關。

- 通道 4 被用來將控制器1與控制器2級聯起來。

- 通道 0-3 是字節操做,地址/計數都是字節的。

- 通道 5-7 是字操做,地址/計數都是以字爲單位的。

- 傳輸器對於(0-3通道)必須不超過64K的物理邊界,對於5-7必須不超過128K邊界。

- 對於5-7通道page registers 不用數據 bit 0, 表明128K頁

- 對於0-3通道page registers 使用 bit 0, 表示 64K頁

DMA 傳輸器限制在低於16M物理內存裏。裝入寄存器的地址必須是物理地址,而不是邏輯地址。

在include/asm-i386/dma.h中有i386平臺的8237 DMA控制器的各處寄存器的地址及寄存器的定義,這裏只對控制寄存器加以說明:

DMA Channel Control/Status Register (DCSRX)

第31位 代表是否開始

第30位選定Descriptor和Non-Descriptor模式

第29位 判斷有無中斷

第8位 請求處理 (Request Pending)

第3位 Channel是否運行

第2位 當前數據交換是否完成

第1位是否由Descriptor產生中斷

第0位 是否由總線錯誤引發中斷



DMA通道使用的地址

DMA通道用dma_chan結構數組表示,這個結構在kernel/dma.c中,列出以下:

  1. struct dma_chan {  
  2.     int lock;  
  3.     const char *device_id;  
  4. };  
  5.   
  6. static struct dma_chan dma_chan_busy[MAX_DMA_CHANNELS] = {  
  7.     [4] = { 1, "cascade" },  
  8. };  


若是dma_chan_busy[n].lock != 0表示忙,DMA0保留爲DRAM更新用,DMA4用做級聯。DMA 緩衝區的主要問題是,當它大於一頁時,它必須佔據物理內存中的連續頁。

因爲DMA須要連續的內存,於是在引導時分配內存或者爲緩衝區保留物理 RAM 的頂部。在引導時給內核傳遞一個"mem="參數能夠保留 RAM 的頂部。例如,若是系統有 32MB 內存,參數"mem=31M"阻止內核使用最頂部的一兆字節。稍後,模塊能夠使用下面的代碼來訪問這些保留的內存:

  1. dmabuf = ioremap( 0x1F00000 /* 31M */, 0x100000 /* 1M */);  


分配 DMA 空間的方法,代碼調用 kmalloc(GFP_ATOMIC) 直到失敗爲止,而後它等待內核釋放若干頁面,接下來再一次進行分配。最終會發現由連續頁面組成的DMA 緩衝區的出現。

一個使用 DMA 的設備驅動程序一般會與鏈接到接口總線上的硬件通信,這些硬件使用物理地址,而程序代碼使用虛擬地址。基於 DMA 的硬件使用總線地址而不是物理地址,有時,接口總線是經過將 I/O 地址映射到不一樣物理地址的橋接電路鏈接的。甚至某些系統有一個頁面映射方案,可以使任意頁面在外圍總線上表現爲連續的。

當驅動程序須要向一個 I/O 設備(例如擴展板或者DMA控制器)發送地址信息時,必須使用 virt_to_bus 轉換,在接受到來自鏈接到總線上硬件的地址信息時,必須使用 bus_to_virt 了。



DMA操做函數

由於 DMA 控制器是一個系統級的資源,因此內核協助處理這一資源。內核使用 DMA 註冊表爲 DMA 通道提供了請求/釋放機制,而且提供了一組函數在 DMA 控制器中配置通道信息。

DMA 控制器使用函數request_dma和free_dma來獲取和釋放 DMA 通道的全部權,請求 DMA 通道應在請求了中斷線以後,而且在釋放中斷線以前釋放它。每個使用 DMA 的設備也必須使用中斷信號線,不然就沒法發出數據傳輸完成的通知。這兩個函數的聲明列出以下(在kernel/dma.c中):

  1. int request_dma(unsigned int channel, const char *name);  
  2. void free_dma(unsigned int channel);  


DMA 控制器被dma_spin_lock 的自旋鎖所保護。使用函數claim_dma_lock和release_dma_lock對得到和釋放自旋鎖。這兩個函數的聲明列出以下(在kernel/dma.c中):

unsigned long claim_dma_lock(); 獲取 DMA 自旋鎖,該函數會阻塞本地處理器上的中斷,所以,其返回值是"標誌"值,在從新打開中斷時必須使用該值。

void release_dma_lock(unsigned long flags); 釋放 DMA 自旋鎖,而且恢復之前的中斷狀態。

DMA 控制器的控制設置信息由RAM 地址、傳輸的數據(以字節或字爲單位),以及傳輸的方向三部分組成。下面是i386平臺的8237 DMA控制器的操做函數說明(在include/asm-i386/dma.h中),使用這些函數設置DMA控制器時,應該持有自旋鎖。但在驅動程序作I/O 操做時,不能持有自旋鎖。

void set_dma_mode(unsigned int channel, char mode); 該函數指出通道從設備讀(DMA_MODE_WRITE)或寫(DMA_MODE_READ)數據方式,當mode設置爲 DMA_MODE_CASCADE時,表示釋放對總線的控制。

void set_dma_addr(unsigned int channel, unsigned int addr); 函數給 DMA 緩衝區的地址賦值。該函數將 addr 的最低 24 位存儲到控制器中。參數 addr 是總線地址。

void set_dma_count(unsigned int channel, unsigned int count);該函數對傳輸的字節數賦值。參數 count 也表明 16 位通道的字節數,在此狀況下,這個數字必須是偶數。

除了這些操做函數外,還有些對DMA狀態進行控制的工具函數:

void disable_dma(unsigned int channel); 該函數設置禁止使用DMA 通道。這應該在配置 DMA 控制器以前設置。

void enable_dma(unsigned int channel); 在DMA 通道中包含了合法的數據時,該函數激活DMA 控制器。

int get_dma_residue(unsigned int channel); 該函數查詢一個 DMA 傳輸還有多少字節還沒傳輸完。函數返回沒傳完的字節數。當傳輸成功時,函數返回值是0。

void clear_dma_ff(unsigned int channel) 該函數清除 DMA 觸發器(flip-flop),該觸發器用來控制對 16 位寄存器的訪問。能夠經過兩個連續的 8 位操做來訪問這些寄存器,觸發器被清除時用來選擇低字節,觸發器被置位時用來選擇高字節。在傳輸 8 位後,觸發器會自動反轉;在訪問 DMA 寄存器以前,程序員必須清除觸發器(將它設置爲某個已知狀態)。



DMA映射

一個DMA映射就是分配一個 DMA 緩衝區併爲該緩衝區生成一個可以被設備訪問的地址的組合操做。通常狀況下,簡單地調用函數virt_to_bus 就設備總線上的地址,但有些硬件映射寄存器也被設置在總線硬件中。映射寄存器(mapping register)是一個相似於外圍設備的虛擬內存等價物。在使用這些寄存器的系統上,外圍設備有一個相對較小的、專用的地址區段,能夠在此區段執行 DMA。經過映射寄存器,這些地址被重映射到系統 RAM。映射寄存器具備一些好的特性,包括使分散的頁面在設備地址空間看起來是連續的。但不是全部的體系結構都有映射寄存器,特別地,PC 平臺沒有映射寄存器。

在某些狀況下,爲設備設置有用的地址也意味着須要構造一個反彈(bounce)緩衝區。例如,當驅動程序試圖在一個不能被外圍設備訪問的地址(一個高端內存地址)上執行 DMA 時,反彈緩衝區被建立。而後,按照須要,數據被複制到反彈緩衝區,或者從反彈緩衝區複製。

根據 DMA 緩衝區指望保留的時間長短,PCI 代碼區分兩種類型的 DMA 映射:

一致 DMA 映射 它們存在於驅動程序的生命週期內。一個被一致映射的緩衝區必須同時可被 CPU 和外圍設備訪問,這個緩衝區被處理器寫時,可當即被設備讀取而沒有cache效應,反之亦然,使用函數pci_alloc_consistent創建一致映射。

流式 DMA映射 流式DMA映射是爲單個操做進行的設置。它映射處理器虛擬空間的一塊地址,以至它能被設備訪問。應儘量使用流式映射,而不是一致映射。這是由於在支持一致映射的系統上,每一個 DMA 映射會使用總線上一個或多個映射寄存器。具備較長生命週期的一致映射,會獨佔這些寄存器很長時間――即便它們沒有被使用。使用函數dma_map_single創建流式映射。

(1)創建一致 DMA 映射

函數pci_alloc_consistent處理緩衝區的分配和映射,函數分析以下(在include/asm-generic/pci-dma-compat.h中):

  1. static inline void *pci_alloc_consistent(struct pci_dev *hwdev, size_t size, dma_addr_t *dma_handle)  
  2. {  
  3.      return dma_alloc_coherent(hwdev == NULL ? NULL : &hwdev->dev, size, dma_handle, GFP_ATOMIC);  
  4. }  


結構dma_coherent_mem定義了DMA一致性映射的內存的地址、大小和標識等。結構dma_coherent_mem列出以下(在arch/i386/kernel/pci-dma.c中):

  1. struct dma_coherent_mem {  
  2.     void *virt_base;  
  3.     u32 device_base;  
  4.     int size;  
  5.     int flags;  
  6.     unsigned long *bitmap;  
  7. };  


函數dma_alloc_coherent分配size字節的區域的一致內存,獲得的dma_handle是指向分配的區域的地址指針,這個地址做爲區域的物理基地址。dma_handle是與總線同樣的位寬的無符號整數。 函數dma_alloc_coherent分析以下(在arch/i386/kernel/pci-dma.c中):

  1. void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, int gfp)  
  2. {  
  3.     void *ret; //如果設備,獲得設備的dma內存區域,即mem= dev->dma_mem  
  4.     struct dma_coherent_mem *mem = dev ? dev->dma_mem : NULL;  
  5.     int order = get_order(size);//將size轉換成order,即忽略特定的區域,於是忽略這兩個標識  
  6.     gfp &= ~(__GFP_DMA | __GFP_HIGHMEM);  
  7.   
  8.     if (mem) { //設備的DMA映射,mem= dev->dma_mem  
  9.         //找到mem對應的頁  
  10.         int page = bitmap_find_free_region(mem->bitmap, mem->size, order);  
  11.         if (page >= 0) {  
  12.             *dma_handle = mem->device_base + (page << PAGE_SHIFT);  
  13.             ret = mem->virt_base + (page << PAGE_SHIFT);  
  14.             memset(ret, 0, size);  
  15.             return ret;  
  16.        }  
  17.         if (mem->flags & DMA_MEMORY_EXCLUSIVE)  
  18.         return NULL;  
  19.     }  
  20.     //不是設備的DMA映射  
  21.     if (dev == NULL || (dev->coherent_dma_mask < 0xffffffff))  
  22.         gfp |= GFP_DMA;  
  23.     //分配空閒頁  
  24.     ret = (void *)__get_free_pages(gfp, order);  
  25.   
  26.     if (ret != NULL) {  
  27.         memset(ret, 0, size);  //清0  
  28.         *dma_handle = virt_to_phys(ret);  //獲得物理地址  
  29.     }  
  30.     return ret;  
  31. }  


當再也不須要緩衝區時(一般在模塊卸載時),應該調用函數 pci_free_consitent 將它返還給系統。

(2)創建流式 DMA 映射

在流式 DMA 映射的操做中,緩衝區傳送方向應匹配於映射時給定的方向值。緩衝區被映射後,它就屬於設備而再也不屬於處理器了。在緩衝區調用函數pci_unmap_single撤銷映射以前,驅動程序不該該觸及其內容。

在緩衝區爲 DMA 映射時,內核必須確保緩衝區中全部的數據已經被實際寫到內存。可能有些數據還會保留在處理器的高速緩衝存儲器中,所以必須顯式刷新。在刷新以後,由處理器寫入緩衝區的數據對設備來講也許是不可見的。

若是欲映射的緩衝區位於設備不能訪問的內存區段時,某些體系結構僅僅會操做失敗,而其它的體系結構會建立一個反彈緩衝區。反彈緩衝區是被設備訪問的獨立內存區域,反彈緩衝區複製原始緩衝區的內容。

函數pci_map_single映射單個用於傳送的緩衝區,返回值是能夠傳遞給設備的總線地址,若是出錯的話就爲 NULL。一旦傳送完成,應該使用函數pci_unmap_single 刪除映射。其中,參數direction爲傳輸的方向,取值以下:

PCI_DMA_TODEVICE 數據被髮送到設備。

PCI_DMA_FROMDEVICE若是數據將發送到 CPU。

PCI_DMA_BIDIRECTIONAL數據進行兩個方向的移動。

PCI_DMA_NONE 這個符號只是爲幫助調試而提供。

函數pci_map_single分析以下(在arch/i386/kernel/pci-dma.c中):

  1. static inline dma_addr_t pci_map_single(struct pci_dev *hwdev, void *ptr, size_t size, int direction)  
  2. {  
  3.     return dma_map_single(hwdev == NULL ? NULL : &hwdev->dev, ptr, size, (enum ma_data_direction)direction);  
  4. }  


函數dma_map_single映射一塊處理器虛擬內存,這塊虛擬內存能被設備訪問,返回內存的物理地址,函數dma_map_single分析以下(在include/asm-i386/dma-mapping.h中):

  1. static inline dma_addr_t dma_map_single(struct device *dev, void *ptr, size_t size, enum dma_data_direction direction)  
  2. {  
  3.     BUG_ON(direction == DMA_NONE);  
  4.     //可能有些數據還會保留在處理器的高速緩衝存儲器中,所以必須顯式刷新  
  5.     flush_write_buffers();  
  6.     return virt_to_phys(ptr);//虛擬地址轉化爲物理地址  
  7. }  


(3)分散/集中映射

分散/集中映射是流式 DMA 映射的一個特例。它將幾個緩衝區集中到一塊兒進行一次映射,並在一個 DMA 操做中傳送全部數據。這些分散的緩衝區由分散表結構scatterlist來描述,多個分散的緩衝區的分散表結構組成緩衝區的struct scatterlist數組。

分散表結構列出以下(在include/asm-i386/scatterlist.h):

  1. struct scatterlist {  
  2.     struct page *page;  
  3.     unsigned int offset;  
  4.     dma_addr_t dma_address; //用在分散/集中操做中的緩衝區地址  
  5.     unsigned int length;//該緩衝區的長度  
  6. };  


每個緩衝區的地址和長度會被存儲在 struct scatterlist 項中,但在不一樣的體系結構中它們在結構中的位置是不一樣的。下面的兩個宏定義來解決平臺移植性問題,這些宏定義應該在一個pci_map_sg 被調用後使用:

  1. #define sg_dma_address(sg) ((sg)->dma_address)  //從該分散表項中返回總線地址  
  2. #define sg_dma_len(sg) ((sg)->length)  //返回該緩衝區的長度  


函數pci_map_sg完成分散/集中映射,其返回值是要傳送的 DMA 緩衝區數;它可能會小於 nents(也就是傳入的分散表項的數量),由於可能有的緩衝區地址上是相鄰的。一旦傳輸完成,分散/集中映射經過調用函數pci_unmap_sg 來撤銷映射。 函數pci_map_sg分析以下(在include/asm-generic/pci-dma-compat.h中):

  1. static inline int pci_map_sg(struct pci_dev *hwdev, struct scatterlist *sg, int nents, int direction)  
  2. {  
  3.     return dma_map_sg(hwdev == NULL ? NULL : &hwdev->dev, sg, nents, (enum dma_data_direction)direction);  
  4. }  
  5.   
  6. //include/asm-i386/dma-mapping.h  
  7. static inline int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction)  
  8. {  
  9.     int i;  
  10.     BUG_ON(direction == DMA_NONE);  
  11.     for (i = 0; i < nents; i++ ) {  
  12.         BUG_ON(!sg[i].page);  
  13.         //將頁及頁偏移地址轉化爲物理地址  
  14.         sg[i].dma_address = page_to_phys(sg[i].page) + sg[i].offset;  
  15.     }  
  16.     //可能有些數據還會保留在處理器的高速緩衝存儲器中,所以必須顯式刷新  
  17.     flush_write_buffers();  
  18.     return nents;  
  19. }  


DMA池

許多驅動程序須要又多又小的一致映射內存區域給DMA描述子或I/O緩存buffer,這使用DMA池比用dma_alloc_coherent分配的一頁或多頁內存區域好,DMA池用函數dma_pool_create建立,用函數dma_pool_alloc從DMA池中分配一塊一致內存,用函數dmp_pool_free放內存回到DMA池中,使用函數dma_pool_destory釋放DMA池的資源。

結構dma_pool是DMA池描述結構,列出以下:

  1. struct dma_pool { /* the pool */  
  2.     struct list_head page_list;//頁鏈表  
  3.     spinlock_t lock;  
  4.     size_t blocks_per_page;//每頁的塊數  
  5.     size_t size; //DMA池裏的一致內存塊的大小  
  6.     struct device *dev; //將作DMA的設備  
  7.     size_t allocation; //分配的沒有跨越邊界的塊數,是size的整數倍  
  8.     char name [32];//池的名字  
  9.     wait_queue_head_t waitq; //等待隊列  
  10.     struct list_head pools;  
  11. };  


函數dma_pool_create給DMA建立一個一致內存塊池,其參數name是DMA池的名字,用於診斷用,參數dev是將作DMA的設備,參數size是DMA池裏的塊的大小,參數align是塊的對齊要求,是2的冪,參數allocation返回沒有跨越邊界的塊數(或0)。

函數dma_pool_create返回建立的帶有要求字符串的DMA池,若建立失敗返回null。對被給的DMA池,函數dma_pool_alloc被用來分配內存,這些內存都是一致DMA映射,可被設備訪問,且沒有使用緩存刷新機制,由於對齊緣由,分配的塊的實際尺寸比請求的大。若是分配非0的內存,從函數dma_pool_alloc返回的對象將不跨越size邊界(如不跨越4K字節邊界)。這對在個體的DMA傳輸上有地址限制的設備來講是有利的。

函數dma_pool_create分析以下(在drivers/base/dmapool.c中):

  1. struct dma_pool *dma_pool_create (const char *name, struct device *dev, size_t size, size_t align, size_t allocation)  
  2. {  
  3.     struct dma_pool *retval;  
  4.     if (align == 0)  
  5.         align = 1;  
  6.     if (size == 0)  
  7.         return NULL;  
  8.     else if (size < align)  
  9.         size = align;  
  10.     else if ((size % align) != 0) {//對齊處理  
  11.         size += align + 1;  
  12.         size &= ~(align - 1);  
  13.     }  
  14.     //若是一致內存塊比頁大,是分配爲一致內存塊大小,不然,分配爲頁大小  
  15.     if (allocation == 0) {  
  16.         if (PAGE_SIZE < size)//頁比一致內存塊小  
  17.             allocation = size;  
  18.         else  
  19.             allocation = PAGE_SIZE;//頁大小  
  20.     // FIXME: round up for less fragmentation  
  21.     } else if (allocation < size)  
  22.         return NULL;  
  23.     //分配dma_pool結構對象空間  
  24.     if (!(retval = kmalloc (sizeof *retval, SLAB_KERNEL)))  
  25.     return retval;  
  26.   
  27.     strlcpy (retval->name, name, sizeof retval->name);  
  28.   
  29.     retval->dev = dev;  
  30.     //初始化dma_pool結構對象retval  
  31.     INIT_LIST_HEAD (&retval->page_list);//初始化頁鏈表  
  32.     spin_lock_init (&retval->lock);  
  33.     retval->size = size;  
  34.     retval->allocation = allocation;  
  35.     retval->blocks_per_page = allocation / size;  
  36.     init_waitqueue_head (&retval->waitq);//初始化等待隊列  
  37.   
  38.     if (dev) {  
  39.         down (&pools_lock);  
  40.         if (list_empty (&dev->dma_pools))  
  41.         //給設備建立sysfs文件系統屬性文件  
  42.             device_create_file (dev, &dev_attr_pools);  
  43.             /* note: not currently insisting "name" be unique */  
  44.             list_add (&retval->pools, &dev->dma_pools);//將DMA池加到dev中  
  45.             up (&pools_lock);  
  46.     } else  
  47.         INIT_LIST_HEAD (&retval->pools);  
  48.     return retval;  
  49. }  


函數dma_pool_alloc從DMA池中分配一塊一致內存,其參數pool是將產生塊的DMA池,參數mem_flags是GFP_*位掩碼,參數handle是指向塊的DMA地址,函數dma_pool_alloc返回當前沒用的塊的內核虛擬地址,並經過handle給出它的DMA地址,若是內存塊不能被分配,返回null。

函數dma_pool_alloc包裹了dma_alloc_coherent頁分配器,這樣小塊更容易被總線的主控制器使用。這可能共享slab分配器的內容。

函數dma_pool_alloc分析以下(在drivers/base/dmapool.c中):

  1. void *dma_pool_alloc (struct dma_pool *pool, int mem_flags, dma_addr_t *handle)  
  2. {  
  3.     unsigned long flags;  
  4.     struct dma_page *page;  
  5.     int map, block;  
  6.     size_t offset;  
  7.     void *retval;  
  8.   
  9. restart:  
  10.     spin_lock_irqsave (&pool->lock, flags);  
  11.     list_for_each_entry(page, &pool->page_list, page_list) {  
  12.         int i;  
  13.         /* only cachable accesses here ... */  
  14.         //遍歷一頁的每塊,而每塊又以32字節遞增  
  15.         for (map = 0, i = 0; i < pool->blocks_per_page;/*每頁的塊數*/ i += BITS_PER_LONG, map++) {// BITS_PER_LONG定義爲32  
  16.             if (page->bitmap [map] == 0)  
  17.                 continue;  
  18.             block = ffz (~ page->bitmap [map]);//找出第一個0  
  19.             if ((i + block) < pool->blocks_per_page) {  
  20.                 clear_bit (block, &page->bitmap [map]);  
  21.                 //獲得相對於頁邊界的偏移  
  22.                 offset = (BITS_PER_LONG * map) + block;  
  23.                 offset *= pool->size;  
  24.                 goto ready;  
  25.             }  
  26.         }  
  27.     }  
  28.     //給DMA池分配dma_page結構空間,加入到pool->page_list鏈表,並做DMA一致映射,它包括分配給DMA池一頁。  
  29.     //SLAB_ATOMIC表示調用 kmalloc(GFP_ATOMIC)直到失敗爲止,而後它等待內核釋放若干頁面,接下來再一次進行分配。  
  30.     if (!(page = pool_alloc_page (pool, SLAB_ATOMIC))) {  
  31.         if (mem_flags & __GFP_WAIT) {  
  32.             DECLARE_WAITQUEUE (wait, current);  
  33.             current->state = TASK_INTERRUPTIBLE;  
  34.             add_wait_queue (&pool->waitq, &wait);  
  35.             spin_unlock_irqrestore (&pool->lock, flags);  
  36.             schedule_timeout (POOL_TIMEOUT_JIFFIES);  
  37.             remove_wait_queue (&pool->waitq, &wait);  
  38.             goto restart;  
  39.         }  
  40.         retval = NULL;   
  41.         goto done;  
  42.     }  
  43.     clear_bit (0, &page->bitmap [0]);  
  44.     offset = 0;  
  45.   
  46. ready:  
  47.     page->in_use++;  
  48.     retval = offset + page->vaddr;//返回虛擬地址  
  49.     *handle = offset + page->dma;//相對DMA地址  
  50.     #ifdef CONFIG_DEBUG_SLAB   
  51.         memset (retval, POOL_POISON_ALLOCATED, pool->size);  
  52.     #endif  
  53.   
  54. done:  
  55.     spin_unlock_irqrestore (&pool->lock, flags);  
  56.     return retval;  
  57. }  


一個簡單的使用DMA 例子

示例:下面是一個簡單的使用DMA進行傳輸的驅動程序,它是一個假想的設備,只列出DMA相關的部分來講明驅動程序中如何使用DMA的。

函數dad_transfer是設置DMA對內存buffer的傳輸操做函數,它使用流式映射將buffer的虛擬地址轉換到物理地址,設置好DMA控制器,而後開始傳輸數據。

    1. int dad_transfer(struct dad_dev *dev, int write, void *buffer, size_t count)  
    2. {  
    3.     dma_addr_t bus_addr;  
    4.     unsigned long flags;  
    5.   
    6.     /* Map the buffer for DMA */  
    7.     dev->dma_dir = (write ? PCI_DMA_TODEVICE : PCI_DMA_FROMDEVICE);  
    8.     dev->dma_size = count;  
    9.     //流式映射,將buffer的虛擬地址轉化成物理地址  
    10.     bus_addr = pci_map_single(dev->pci_dev, buffer, count, dev->dma_dir);  
    11.     dev->dma_addr = bus_addr; //DMA傳送的buffer物理地址  
    12.   
    13.     //將操做控制寫入到DMA控制器寄存器,從而創建起設備  
    14.     writeb(dev->registers.command, DAD_CMD_DISABLEDMA);  
    15.     //設置傳輸方向--讀仍是寫  
    16.     writeb(dev->registers.command, write ? DAD_CMD_WR : DAD_CMD_RD);  
    17.     writel(dev->registers.addr, cpu_to_le32(bus_addr));//buffer物理地址  
    18.     writel(dev->registers.len, cpu_to_le32(count)); //傳輸的字節數  
    19.   
    20.     //開始激活DMA進行數據傳輸操做  
    21.     writeb(dev->registers.command, DAD_CMD_ENABLEDMA);  
    22.     return 0;  
    23. }  
    24.   
    25. //函數dad_interrupt是中斷處理函數,當DMA傳輸完時,調用這個中斷函數來取消buffer上的DMA映射,從而讓內核程序能夠訪問這個buffer。  
    26. void dad_interrupt(int irq, void *dev_id, struct pt_regs *regs)  
    27. {  
    28.     struct dad_dev *dev = (struct dad_dev *) dev_id;  
    29.     /* Make sure it's really our device interrupting */  
    30.     /* Unmap the DMA buffer */  
    31.     pci_unmap_single(dev->pci_dev, dev->dma_addr, dev->dma_size, dev->dma_dir);  
    32.   
    33.     /* Only now is it safe to access the buffer, copy to user, etc. */  
    34.     ...  
    35. }  
    36.   
    37. //函數dad_open打開設備,此時應申請中斷號及DMA通道。  
    38. int dad_open (struct inode *inode, struct file *filp)  
    39. {  
    40.     struct dad_device *my_device;  
    41.     // SA_INTERRUPT表示快速中斷處理且不支持共享 IRQ 信號線  
    42.     if ( (error = request_irq(my_device.irq, dad_interrupt, SA_INTERRUPT, "dad", NULL)) )  
    43.         return error; /* or implement blocking open */  
    44.   
    45.     if ( (error = request_dma(my_device.dma, "dad")) ) {  
    46.         free_irq(my_device.irq, NULL);  
    47.         return error; /* or implement blocking open */  
    48.     }  
    49.     return 0;  
    50. }  
    51.   
    52. //在與open 相對應的 close 函數中應該釋放DMA及中斷號。  
    53. void dad_close (struct inode *inode, struct file *filp)  
    54. {  
    55.     struct dad_device *my_device;  
    56.     free_dma(my_device.dma);  
    57.     free_irq(my_device.irq, NULL);  
    58. ……  
    59. }  
    60.   
    61. //函數dad_dma_prepare初始化DMA控制器,設置DMA控制器的寄存器的值,爲 DMA 傳輸做準備。  
    62. int dad_dma_prepare(int channel, int mode, unsigned int buf, unsigned int count)  
    63. {  
    64.     unsigned long flags;  
    65.     flags = claim_dma_lock();  
    66.     disable_dma(channel);  
    67.     clear_dma_ff(channel);  
    68.     set_dma_mode(channel, mode);  
    69.     set_dma_addr(channel, virt_to_bus(buf));  
    70.     set_dma_count(channel, count);  
    71.     enable_dma(channel);  
    72.     release_dma_lock(flags);  
    73.     return 0;  
    74. }  
    75.   
    76. //函數dad_dma_isdone用來檢查 DMA 傳輸是否成功結束。  
    77. int dad_dma_isdone(int channel)  
    78. {  
    79.     int residue;  
    80.     unsigned long flags = claim_dma_lock ();  
    81.     residue = get_dma_residue(channel);  
    82.     release_dma_lock(flags);  
    83.     return (residue == 0);  

 

 

 

 

Linux 中斷詳解 【轉】

方法之三:以數據結構爲基點,舉一反三

  結構化程序設計思想認爲:程序 =數據結構 +算法。數據結構體現了整個系統的構架,因此數據結構一般都是代碼分析的很好的着手點,對Linux內核分析尤爲如此。好比,把進程控制塊結構分析清楚 了,就對進程有了基本的把握;再好比,把頁目錄結構和頁表結構弄懂了,兩級虛存映射和內存管理也就掌握得差很少了。爲了體現按部就班的思想,在這我就以 Linux對中斷機制的處理來介紹這種方法。

  首先,必須指出的是:在此處,中斷指廣義的中斷概義,它指全部經過idt進行的控制轉移的機制和處理;它覆蓋如下幾個經常使用的概義:中斷、異常、可屏蔽中斷、不可屏蔽中斷、硬中斷、軟中斷 … … …

I、硬件提供的中斷機制和約定

一.中斷向量尋址:

  硬件提供可供256個服務程序中斷進入的入口,即中斷向量;

  中斷向量在保護模式下的實現機制是中斷描述符表idt,idt的位置由idtr肯定,idtr是個48位的寄存器,高32位是idt的基址,低16位爲idt的界限(一般爲2k=256*8);

  idt中包含256箇中斷描述符,對應256箇中斷向量;每一箇中斷描述符8位,其結構如圖一:

Linux 中斷詳解 - 海飛絲 - 風十二的博客

  中斷進入過程如圖二所示。

  當中斷是由低特權級轉到高特權級(即當前特權級CPL>DPL)時,將進行堆棧的轉移;內層堆棧的選擇由當前tss的相應字段肯定,並且內層堆棧將依次被壓入以下數據:外層SS,外層ESP,EFLAGS,外層CS,外層EIP; 中斷返回過程爲一逆過程;

Linux 中斷詳解 - 海飛絲 - 風十二的博客

二.異常處理機制:

  Intel公司保留0-31號中斷向量用來處理異常事件:當產生一個異常時,處理機就會自動把控制轉移到相應的處理程序的入口,異常的處理程序由操做系統提供,中斷向量和異常事件對應如表一:

表1、中斷向量和異常事件對應表

 

中斷向量號 異常事件 Linux的處理程序
0 除法錯誤 Divide_error
1 調試異常 Debug
2 NMI中斷 Nmi
3 單字節,int 3 Int3
4 溢出 Overflow
5 邊界監測中斷 Bounds
6 無效操做碼 Invalid_op
7 設備不可用 Device_not_available
8 雙重故障 Double_fault
9 協處理器段溢出 Coprocessor_segment_overrun
10 無效TSS Incalid_tss
11 缺段中斷 Segment_not_present
12 堆棧異常 Stack_segment
13 通常保護異常 General_protection
14 頁異常 Page_fault
15   Spurious_interrupt_bug
16 協處理器出錯 Coprocessor_error
17 對齊檢查中斷 Alignment_check

三.可編程中斷控制器8259A:

  爲更好的處理外部設備,x86微機提供了兩片可編程中斷控制器,用來輔助cpu接受外部的中斷信號;對於中斷,cpu只提供兩個外接引線:NMI和INTR;

  NMI只能經過端口操做來屏蔽,它一般用於:電源掉電和物理存儲器奇偶驗錯;

  INTR可經過直接設置中斷屏蔽位來屏蔽,它可用來接受外部中斷信號,但只有一個引線,不夠用;因此它經過外接兩片級鏈了的8259A,以接受更多的外部中斷信號。8259A主要完成這樣一些任務:

 

    1. 中斷優先級排隊管理,
    2. 接受外部中斷請求
    3. 向cpu提供中斷類型號

  外部設備產生的中斷信號在IRQ(中斷請求)管腳上首先由中斷控制器處理。中斷控制器可 以響應多箇中斷輸入,它的輸出鏈接到 CPU 的 INT 管腳,信號可經過INT 管腳,通知處理器產生了中斷。若是 CPU 這時能夠處理中斷,CPU 會經過 INTA(中斷確認)管腳上的信號通知中斷控制器已接受中斷,這時,中斷控制器可將一個 8 位數據放置在數據總線上,這一 8 位數據也稱爲中斷向量號,CPU 依據中斷向量號和中斷描述符表(IDT)中的信息自動調用相應的中斷服務程序。圖三中,兩個中斷控制器級聯了起來,從屬中斷控制器的輸出鏈接到了主中斷控 制器的第 3 箇中斷信號輸入,這樣,該系統可處理的外部中斷數量最多可達 15 個,圖的右邊是 i386 PC 中各中斷輸入管腳的通常分配。可經過對8259A的初始化,使這15個外接引腳對應256箇中斷向量的任何15個連續的向量;因爲intel公司保留0- 31號中斷向量用來處理異常事件(而默認狀況下,IBM bios把硬中斷設在0x08-0x0f),因此,硬中斷必須設在31之後,linux則在實模式下初始化時把其設在0x20-0x2F,對此下面還將具 體說明。

圖3、i386 PC 可編程中斷控制器8259A級鏈示意圖

Linux 中斷詳解 - 海飛絲 - 風十二的博客

II、Linux的中斷處理

  硬件中斷機制提供了256個入口,即idt中包含的256箇中斷描述符(對應256箇中斷向量)。

  而0-31號中斷向量被intel公司保留用來處理異常事件,不能另做它用。對這 0-31號中斷向量,操做系統只需提供異常的處理程序,當產生一個異常時,處理機就會自動把控制轉移到相應的處理程序的入口,運行相應的處理程序;而事實 上,對於這32個處理異常的中斷向量,此版本(2.2.5)的 Linux只提供了0-17號中斷向量的處理程序,其對應處理程序參見表1、中斷向量和異常事件對應表;也就是說,17-31號中斷向量是空着未用的。

  既然0-31號中斷向量已被保留,那麼,就是剩下32-255共224箇中斷向量可用。 這224箇中斷向量又是怎麼分配的呢?在此版本(2.2.5)的Linux中,除了0x80 (SYSCALL_VECTOR)用做系統調用總入口以外,其餘都用在外部硬件中斷源上,其中包括可編程中斷控制器8259A的15個irq;事實上,當 沒有定義CONFIG_X86_IO_APIC時,其餘223(除0x80外)箇中斷向量,只利用了從32號開始的15個,其它208個空着未用。

  這些中斷服務程序入口的設置將在下面有詳細說明。

一.相關數據結構

  1. 中斷描述符表idt: 也就是中斷向量表,至關如一個數組,保存着各中斷服務例程的入口。(詳細描述參見圖1、中斷描述符格式)
  2. 與硬中斷相關數據結構:

與硬中斷相關數據結構主要有三個:

一:定義在/arch/i386/kernel/irq.h中的

struct hw_interrupt_type {

const char * typename;

void (*startup)(unsigned int irq);

void (*shutdown)(unsigned int irq);

void (*handle)(unsigned int irq, struct pt_regs * regs);

void (*enable)(unsigned int irq);

void (*disable)(unsigned int irq);

};

二:定義在/arch/i386/kernel/irq.h中的

typedef struct {

unsigned int status; /* IRQ status - IRQ_INPROGRESS, IRQ_DISABLED */

struct hw_interrupt_type *handler; /* handle/enable/disable functions */

struct irqaction *action; /* IRQ action list */

unsigned int depth; /* Disable depth for nested irq disables */

} irq_desc_t;

三:定義在include/linux/ interrupt.h中的

struct irqaction {

void (*handler)(int, void *, struct pt_regs *);

unsigned long flags;

unsigned long mask;

const char *name;

void *dev_id;

struct irqaction *next;

};

三者關係以下:

Linux 中斷詳解 - 海飛絲 - 風十二的博客

圖4、與硬中斷相關的幾個數據結構各關係

各結構成員詳述以下:

    1. struct irqaction結構,它包含了內核接收到特定IRQ以後應該採起的操做,其成員以下:
  • handler:是一指向某個函數的指針。該函數就是所在結構對相應中斷的處理函數。
  • flags:取值只有SA_INTERRUPT(中斷可嵌套),SA_SAMPLE_RANDOM(這個中斷是源於物理隨機性的),和SA_SHIRQ(這個IRQ和其它struct irqaction共享)。
  • mask:在x86或者體系結構無關的代碼中不會使用(除非將其設置爲0);只有在SPARC64的移植版本中要跟蹤有關軟盤的信息時纔會使用它。
  • name:產生中斷的硬件設備的名字。由於不止一個硬件能夠共享一個IRQ。
  • dev_id:標識硬件類型的一個惟一的ID。Linux支持的全部硬件設備的每一種類型,都有一個由製造廠商定義的在此成員中記錄的設備ID。
  • next:若是IRQ是共享的,那麼這就是指向隊列中下一個struct irqaction結構的指針。一般狀況下,IRQ不是共享的,所以這個成員就爲空。
    1. struct hw_interrupt_type結構,它是一個抽象的中斷控制器。這包含一系列的指向函數的指針,這些函數處理控制器特有的操做:
  • typename:控制器的名字。
  • startup:容許從給定的控制器的IRQ所產生的事件。
  • shutdown:禁止從給定的控制器的IRQ所產生的事件。
  • handle:根據提供給該函數的IRQ,處理惟一的中斷。
  • enable和disable:這兩個函數基本上和startup和shutdown相同;

     

    1. 另一個數據結構是irq_desc_t,它具備以下成員:
  • status:一個整數。表明IRQ的狀態:IRQ是否被禁止了,有關IRQ的設備當前是否正被自動檢測,等等。
  • handler:指向hw_interrupt_type的指針。
  • action:指向irqaction結構組成的隊列的頭。正常狀況下每一個IRQ只有一個操做,所以連接列表的正常長度是1(或者0)。可是,若是IRQ被兩個或者多個設備所共享,那麼這個隊列中就有多個操做。
  • depth:irq_desc_t的當前用戶的個數。主要是用來保證在中斷處理過程當中IRQ不會被禁止。
  • irq_desc是irq_desc_t 類型的數組。對於每個IRQ都有一個數組入口,即數組把每個IRQ映射到和它相關的處理程序和irq_desc_t中的其它信息。
  1. 與Bottom_half相關的數據結構:

圖5、底半處理數據結構示意圖

Linux 中斷詳解 - 海飛絲 - 風十二的博客 

  • bh_mask_count:計數器。對每一個enable/disable請求嵌套對進行計數。這些請求經過調用enable_bh和 disable_bh實現。每一個禁止請求都增長計數器;每一個使能請求都減少計數器。當計數器達到0時,全部未完成的禁止語句都已經被使能語句所匹配了,因 此下半部分最終被從新使能。(定義在kernel/softirq.c中)
  • bh_mask和bh_active:它們共同決定下半部分是否運行。它們兩個都有32位,而每個下半部分都佔用一位。當一個上半部 分(或者一些其它代碼)決定其下半部分須要運行時,就經過設置bh_active中的一位來標記下半部分。無論是否作這樣的標記,下半部分均可以經過清空 bh_mask中的相關位來使之失效。所以,對bh_mask和bh_active進行位AND運算就可以代表應該運行哪個下半部分。特別是若是位與運 算的結果是0,就沒有下半部分須要運行。
  • bh_base:是一組簡單的指向下半部分處理函數的指針。

  bh_base表明的指針數組中可包含 32 個不一樣的底半處理程序。bh_mask 和 bh_active 的數據位分別表明對應的底半處理過程是否安裝和激活。若是 bh_mask 的第 N 位爲 1,則說明 bh_base 數組的第 N 個元素包含某個底半處理過程的地址;若是 bh_active 的第 N 位爲 1,則說明必須由調度程序在適當的時候調用第 N 個底半處理過程。 

二. 向量的設置和相關數據的初始化:

    • 在實模式下的初始化過程當中,經過對中斷控制器8259A-1,9259A-2從新編程,把硬中斷設到0x20-0x2F。即把IRQ0& #0;IRQ15分別與0x20-0x2F號中斷向量對應起來;當對應的IRQ發生了時,處理機就會經過相應的中斷向量,把控制轉到對應的中斷服務例 程。(源碼在Arch/i386/boot/setup.S文件中;相關內容可參見 實模式下的初始化 部分)
    • 在保護模式下的初始化過程當中,設置並初始化idt,共256個入口,服務程序均爲ignore_int, 該服務程序僅打印「Unknown interruptn」。(源碼參見Arch/i386/KERNEL/head.S文件;相關內容可參見 保護模式下的初始化 部分)
    • 在系統初始化完成後運行的第一個內核程序asmlinkage void __init start_kernel(void) (源碼在文件init/main.c中) 中,經過調用void __init trap_init(void)函數,把各自陷和中斷服務程序的入口地址設置到 idt 表中,即將表一中對應的處理程序入口設置到相應的中斷向量表項中;在此版本(2.2.5)的Linux只設置0-17號中斷向量。(trap_init (void)函數定義在arch/i386/kernel/traps.c 中; 相關內容可參見 詳解系統調用 部分)
    • 在同一個函數void __init trap_init(void)中,經過調用函數set_system_gate(SYSCALL_VECTOR,&system_call); 把系統調用總控程序的入口掛在中斷0x80上。其中SYSCALL_VECTOR是定義在 linux/arch/i386/kernel/irq.h中的一個常量0x80; 而 system_call 即爲中斷總控程序的入口地址;中斷總控程序用匯編語言定義在arch/i386/kernel/entry.S中。(相關內容可參見 詳解系統調用 部分)
    • 在系統初始化完成後運行的第一個內核程序asmlinkage void __init start_kernel(void) (源碼在文件init/main.c中) 中,經過調用void init_IRQ(void)函數,把地址標號interrupt[i](i從1-223)設置到 idt 表中的的32-255號中斷向量(0x80除外),外部硬件IRQ的觸發,將經過這些地址標號最終進入到各自相應的處理程序。(init_IRQ (void)函數定義在arch/i386/kernel/IRQ.c 中;)
    • interrupt[i](i從1-223),是在arch/i386/kernel/IRQ.c文件中,經過一系列嵌套的相似如 BUILD_16_IRQS(0x0)的宏,定義的一系列地址標號;(這些定義interrupt[i]的宏,所有定義在文件 arch/i386/kernel/IRQ.c和arch/i386/kernel/IRQ.H中。這些嵌套的宏的使用,原理很簡單,但很煩,限於篇幅, 在此省略)
    • 各以interrupt[i]爲入口的代碼,在進行一些簡單的處理後,最後都會調用函數asmlinkage void do_IRQ(struct pt_regs regs),do_IRQ函數調用static void do_8259A_IRQ(unsigned int irq, struct pt_regs * regs) 而do_8259A_IRQ在進行必要的處理後,將調用已與此IRQ創建聯繫irqaction中的處理函數,以進行相應的中斷處理。最後處理機將跳轉到 ret_from_intr進行必要處理後,整個中斷處理結束返回。(相關源碼都在文件arch/i386/kernel/IRQ.c和 arch/i386/kernel/IRQ.H中。Irqaction結構參見上面的數據結構說明)

三. Bottom_half處理機制

  在此版本(2.2.5)的Linux中,中斷處理程序從概念上被分爲上半部分(top half)和下半部分(bottom half);在中斷髮生時上半部分的處理過程當即執行,可是下半部分(若是有的話)卻推遲執行。內核把上半部分和下半部分做爲獨立的函數來處理,上半部分 決定其相關的下半部分是否須要執行。必須當即執行的部分必須位於上半部分,而能夠推遲的部分可能屬於下半部分。

  那麼爲何這樣劃分紅兩個部分呢?

  • 一個緣由是要把中斷的總延遲時間最小化。Linux內核定義了兩種類型的中斷,快速的和慢速的,這二者之間的一個區別是慢速中斷自身還能夠被中 斷,而快速中斷則不能。所以,當處理快速中斷時,若是有其它中斷到達;無論是快速中斷仍是慢速中斷,它們都必須等待。爲了儘量快地處理這些其它的中斷, 內核就須要儘量地將處理延遲到下半部分執行。
  • 另一個緣由是,當內核執行上半部分時,正在服務的這個特殊IRQ將會被可編程中斷控制器禁止,因而,鏈接在同一個IRQ上的其它設備 就只有等到該該中斷處理被處理完畢後果才能發出IRQ請求。而採用Bottom_half機制後,不須要當即處理的部分就能夠放在下半部分處理,從而,加 快了處理機對外部設備的中斷請求的響應速度。
  • 還有一個緣由就是,處理程序的下半部分還能夠包含一些並不是每次中斷都必須處理的操做;對這些操做,內核能夠在一系列設備中斷以後集中處 理一次就能夠了。即在這種狀況下,每次都執行並不是必要的操做徹底是一種浪費,而採用Bottom_half機制後,能夠稍稍延遲並在後來只執行一次就行 了。

  因而可知,沒有必要每次中斷都調用下半部分;只有bh_mask 和 bh_active的對應位的與爲1時,才必須執行下半部分(do_botoom_half)。因此,若是在上半部分中(也可能在其餘地方)決定必須執行 對應的半部分,那麼能夠經過設置bh_active的對應位,來指明下半部分必須執行。固然,若是bh_active的對應位被置位,也不必定會立刻執行 下半部分,由於還必須具有另外兩個條件:首先是bh_mask的相應位也必須被置位,另外,就是處理的時機,若是下半部分已經標記過須要執行了,如今又再 次標記,那麼內核就簡單地保持這個標記;當狀況容許的時候,內核就對它進行處理。若是在內核有機會運行其下半部分以前給定的設備就已經發生了100次中 斷,那麼內核的上半部分就運行100次,下半部分運行1次。

  bh_base數組的索引是靜態定義的,定時器底半處理過程的地址保存在第 0 個元素中,控制檯底半處理過程的地址保存在第 1 個元素中,等等。當 bh_mask 和 bh_active 代表第 N 個底半處理過程已被安裝且處於活動狀態,則調度程序會調用第 N 個底半處理過程,該底半處理過程最終會處理與之相關的任務隊列中的各個任務。由於調度程序從第 0 個元素開始依次檢查每一個底半處理過程,所以,第 0 個底半處理過程具備最高的優先級,第 31 個底半處理過程的優先級最低。

  內核中的某些底半處理過程是和特定設備相關的,而其餘一些則更通常一些。表二列出了內核中通用的底半處理過程。

表2、Linux 中通用的底半處理過程

 

TIMER_BH(定時器) 在每次系統的週期性定時器中斷中,該底半處理過程被標記爲活動狀態,並用來驅動內核的定時器隊列機制。
CONSOLE_BH(控制檯) 該處理過程用來處理控制檯消息。
TQUEUE_BH(TTY 消息隊列) 該處理過程用來處理 tty 消息。
NET_BH(網絡) 用於通常網絡處理,做爲網絡層的一部分
IMMEDIATE_BH(當即) 這是一個通常性處理過程,許多設備驅動程序利用該過程對本身要在隨後處理的任務進行排隊。

  當某個設備驅動程序,或內核的其餘部分須要將任務排隊進行處理時,它將任務添加到適當的 系統隊列中(例如,添加到系統的定時器隊列中),而後通知內核,代表須要進行底半處理。爲了通知內核,只需將 bh_active 的相應數據位置爲 1。例如,若是驅動程序在 immediate 隊列中將某任務排隊,並但願運行 IMMEDIATE 底半處理過程來處理排隊任務,則只需將 bh_active 的第 8 位置爲 1。在每一個系統調用結束並返回調用進程以前,調度程序要檢驗 bh_active 中的每一個位,若是有任何一位爲 1,則相應的底半處理過程被調用。每一個底半處理過程被調用時,bh_active 中的相應爲被清除。bh_active 中的置位只是暫時的,在兩次調用調度程序之間 bh_active 的值纔有意義,若是 bh_active 中沒有置位,則不須要調用任何底半處理過程。

四.中斷處理全過程

  由前面的分析可知,對於0-31號中斷向量,被保留用來處理異常事件;0x80中斷向量用來做爲系統調用的總入口點;而其餘中斷向量,則用來處理外部設備中斷;這三者的處理過程都是不同的。

    1. 異常的處理全過程

      對這0-31號中斷向量,保留用來處理異常事件;操做系統提供相應的異常的處理程序,並在初 始化時把處理程序的入口等級在對應的中斷向量表項中。當產生一個異常時,處理機就會自動把控制轉移到相應的處理程序的入口,運行相應的處理程序,進行相應 的處理後,返回原中斷處。固然,在前面已經提到,此版本(2.2.5)的Linux只提供了0-17號中斷向量的處理程序。

    2. 中斷的處理全過程

        對於0-31號和0x80以外的中斷向量,主要用來處理外部設備中斷;在系統完成初始化後,其中斷處理過程以下:

        當外部設備須要處理機進行中斷服務時,它就會經過中斷控制器要求處理機進行中斷服務。如 果 CPU 這時能夠處理中斷,CPU將根據中斷控制器提供的中斷向量號和中斷描述符表(IDT)中的登記的地址信息,自動跳轉到相應的interrupt[i]地 址;在進行一些簡單的但必要的處理後,最後都會調用函數do_IRQ , do_IRQ函數調用 do_8259A_IRQ 而do_8259A_IRQ在進行必要的處理後,將調用已與此IRQ創建聯繫irqaction中的處理函數,以進行相應的中斷處理。最後處理機將跳轉到 ret_from_intr進行必要處理後,整個中斷處理結束返回。

        從數據結構入手,應該說是分析操做系統源碼最經常使用的和最主要的方法。由於操做系統的幾大功能部件,如進程管理,設備管理,內存管理等等,均可以經過對其相應的數據結構的分析來弄懂其實現機制。很好的掌握這種方法,對分析Linux內核大有裨益。

      方法之四:以功能爲中心,各個擊破

        從功能上看,整個Linux系統可看做有一下幾個部分組成:

    1. 進程管理機制部分;
    2. 內存管理機制部分;
    3. 文件系統部分;
    4. 硬件驅動部分;
    5. 系統調用部分等;

  以功能爲中心、各個擊破,就是指從這五個功能入手,經過源碼分析,找出Linux是怎樣實現這些功能的。

  在這五個功能部件中,系統調用是用戶程序或操做調用核心所提供的功能的接口;也是分析 Linux內核源碼幾個很好的入口點之一。對於那些在dos或 Uinx、Linux下有過C編程經驗的高手尤爲如此。又因爲系統調用相對其它功能而言,較爲簡單,因此,我就以它爲例,但願經過對系統調用的分析,能使 讀者體會到這一方法。

  與系統調用相關的內容主要有:系統調用總控程序,系統調用向量表sys_call_table,以及各系統調用服務程序。下面將對此一一介紹:

    1. 保護模式下的初始化過程當中,設置並初始化idt,共256個入口,服務程序均爲ignore_int, 該服務程序僅打印「Unknown interruptn」。(源碼參見/Arch/i386/KERNEL/head.S文件;相關內容可參見 保護模式下的初始化 部分)
    2. 在系統初始化完成後運行的第一個內核程序start_kernel中,經過調用 trap_init函數,把各自陷和中斷服務程序的入口地址設置到 idt 表中;同時,此函數還經過調用函數set_system_gate 把系統調用總控程序的入口地址掛在中斷0x80上。其中:
  • start_kernel的原型爲void __init start_kernel(void) ,其源碼在文件 init/main.c中;
  • trap_init函數的原型爲void __init trap_init(void),定義在arch/i386/kernel/traps.c 中
  • 函數set_system_gate一樣定義在arch/i386/kernel/traps.c 中,調用原型爲set_system_gate(SYSCALL_VECTOR,&system_call);
  • 其中,SYSCALL_VECTOR是定義在 linux/arch/i386/kernel/irq.h中的一個常量0x80;
  • 而 system_call 即爲系統調用總控程序的入口地址;中斷總控程序用匯編語言定義在arch/i386/kernel/entry.S中。
(其它相關內容可參見 中斷和中斷處理 部分)
  • 系統調用向量表sys_call_table, 是一個含有NR_syscalls=256個單元的數組。它的每一個單元存放着一個系統調用服務程序的入口地址。該數組定義在 /arch/i386/kernel/entry.S中;而NR_syscalls則是一個等於256的宏,定義在 include/linux/sys.h中。
  • 各系統調用服務程序則分別定義在各個模塊的相應文件中;例如asmlinkage int sys_time(int * tloc)就定義在kerneltime.c中;另外,在kernelsys.c中也有很多服務程序;

    II、系統調用過程

     ∥頤侵潰低車饔檬怯沒С絛蚧蠆僮韉饔煤誦乃峁┑墓δ艿慕涌冢凰韻低車粲玫墓嘆褪譴佑沒С絛虻較低襯諍耍緩笥只氐接沒С絛虻墓蹋輝贚inux中,此過程大致過程可描述以下:

      系統調用過程示意圖:

    Linux 中斷詳解 - 海飛絲 - 風十二的博客

      整個系統調用進入過程客表示以下:

    用戶程序 系統調用總控程序(system_call) 各個服務程序

      可見,系統調用的進入課分爲「用戶程序 系統調用總控程序」和「系統調用總控程序各個服務程序」兩部分;下邊將分別對這兩個部分進行詳細說明:

     

    以程序流程爲線索,適合於分析系統的初始化過程:系統引導、實模式下的初始化、保護模式下的初始化三個部分,和分析應用程序的執行流程:從程序的裝載,到運行,一直到程序的退出。而流程圖則是這種分析方法最合適的表達工具。
    • 「用戶程序 系統調用總控程序」的實現:在前面已經說過,Linux的系統調用使用第0x80號中斷向量項做爲總的入口,也即,系統調用總控程序的入口地址 system_call就掛在中斷0x80上。也就是說,只要用戶程序執行0x80中斷 ( int 0x80 ),就可實現「用戶程序 系統調用總控程序」的進入;事實上,在Linux中,也是這麼作的。只是0x80中斷的執行語句int 0x80 被封裝在標準C庫中,用戶程序只需用標準系統調用函數就能夠了,而不須要在用戶程序中直接寫0x80中斷的執行語句int 0x80。至於中斷的進入的詳細過程可參見前面的「中斷和中斷處理」部分。
    • 「系統調用總控程序 各個服務程序」 的實現:在系統調用總控程序中經過語句「call * SYMBOL_NAME(sys_call_table)(,%eax,4)」來調用各個服務程序(SYMBOL_NAME是定義在 /include/linux/linkage.h中的宏:#define SYMBOL_NAME_LABEL(X) X),能夠忽略)。當系統調用總控程序執行到此語句時,eax中的內容便是相應系統調用的編號,此編號即爲相應服務程序在系統調用向量表 sys_call_table中的編號(關於系統調用的編號說明在/linux/include/asm/unistd.h中)。又由於系統調用向量表 sys_call_table每項佔4個字節,因此由%eax 乘上4造成偏移地址,而sys_call_table則爲基址;基址加上偏移所指向的內容就是相應系統調用服務程序的入口地址。因此此call語句就至關 於直接調用對應的系統調用服務程序。
    • 參數傳遞的實現:在Linux中全部系統調用服務例程都使用了asmlinkage標誌。此標誌是一個定義在/include/linux/linkage.h 中的一個宏:

      #if defined __i386__ && (__GNUC__ > 2 || __GNUC_MINOR__ > 7)

      #define asmlinkage CPP_ASMLINKAGE__attribute__((regparm(0)))

      #else

      #define asmlinkage CPP_ASMLINKAGE

      #endif

      其中涉及到了gcc的一些約定,總之,這個標誌它能夠告訴編譯器該函數不須要從寄存器中得到任何參數,而是從堆棧中取得參數;即參數在堆棧中傳遞,而不是直接經過寄存器;

      堆棧參數以下:

      EBX = 0x00

    ECX = 0x04

    EDX = 0x08

    ESI = 0x0C

    EDI = 0x10

    EBP = 0x14

    EAX = 0x18

    DS = 0x1C

    ES = 0x20

    ORIG_EAX = 0x24

    EIP = 0x28

    CS = 0x2C

    EFLAGS = 0x30

      在進入系統調用總控程序前,用戶按照以上的對應順序將參數放到對應寄存器中,在系統調用 總控程序一開始就將這些寄存器壓入堆棧;在退出總控程序前又按如上順序堆棧;用戶程序則能夠直接從寄存器中復得被服務程序加工過了的參數。而對於系統調用 服務程序而言,參數就能夠直接從總控程序壓入的堆棧中復得;對參數的修改一能夠直接在堆棧中進行;其實,這就是asmlinkage標誌的做用。因此在進 入和退出系統調用總控程序時,「保護現場」和「恢復現場」的內容並不必定會相同。

     

    • 特殊的服務程序:在此版本(2.2.5)的linux內核中,有好幾個系統調用的服務程序都是定義在/usr/src/linux/kernel/sys.c 中的同一個函數:

    asmlinkage int sys_ni_syscall(void)

    {

    return -ENOSYS;

    }

    此函數除了返回錯誤號以外,什麼都沒幹。那他有什麼做用呢?歸結起來有以下三種可能:

    1.處理邊界錯誤,0號系統調用就是用的此特殊的服務程序;

    2.用來替換舊的已淘汰了的系統調用,如: Nr 17, Nr 31, Nr 32, Nr 35, Nr 44, Nr 53, Nr 56, Nr58, Nr 98;

    3. 用於將要擴展的系統調用,如: Nr 137, Nr 188, Nr 189;

    III、系統調用總控程序(system_call)

    系統調用總控程序(system_call)可參見arch/i386/kernel/entry.S其執行流程以下圖:

    Linux 中斷詳解 - 海飛絲 - 風十二的博客

    IV、實例:增長一個系統調用

    由以上的分析可知,增長系統調用因爲下兩種方法:

    i.編一個新的服務例程,將它的入口地址加入到sys_call_table的某一項,只要該項的原服務例程是sys_ni_syscall,而且是sys_ni_syscall的做用屬於第三種的項,也即Nr 137, Nr 188, Nr 189。

    ii.直接增長:

    1. 編一個新的服務例程;
    2. 在sys_call_table中添加一個新項, 並把的新增長的服務例程的入口地址加到sys_call_table表中的新項中;
    3. 把增長的 sys_call_table 表項所對應的向量, 在include/asm-386/unistd.h 中進行必要申明,以供用戶進程和其餘系統進程查詢或調用。
    4. 因爲在標準的c語言庫中沒有新系統調用的承接段,因此,在測試程序中,除了要#include ,還要申明以下 _syscall1(int,additionSysCall,int, num)。

    下面將對第ii種狀況列舉一個我曾經實現過了的一個增長系統調用的實例:

    1.)在kernel/sys.c中增長新的系統服務例程以下:

    asmlinkage int sys_addtotal(int numdata)

    {

    int i=0,enddata=0;

    while(i<=numdata)

    enddata+=i++;

    return enddata;

    }

      該函數有一個 int 型入口參數 numdata , 並返回從 0 到 numdata 的累加值; 固然也能夠把系統服務例程放在一個本身定義的文件或其餘文件中,只是要在相應文件中做必要的說明;

    2.)把 asmlinkage int sys_addtotal( int) 的入口地址加到sys_call_table表中:

    arch/i386/kernel/entry.S 中的最後幾行源代碼修改前爲:

    ... ...

    .long SYMBOL_NAME(sys_sendfile)

    .long SYMBOL_NAME(sys_ni_syscall) /* streams1 */

    .long SYMBOL_NAME(sys_ni_syscall) /* streams2 */

    .long SYMBOL_NAME(sys_vfork) /* 190 */

    .rept NR_syscalls-190

    .long SYMBOL_NAME(sys_ni_syscall)

    .endr

    修改後爲: ... ...

    .long SYMBOL_NAME(sys_sendfile)

    .long SYMBOL_NAME(sys_ni_syscall) /* streams1 */

    .long SYMBOL_NAME(sys_ni_syscall) /* streams2 */

    .long SYMBOL_NAME(sys_vfork) /* 190 */

    /* add by I */

    .long SYMBOL_NAME(sys_addtotal)

    .rept NR_syscalls-191

    .long SYMBOL_NAME(sys_ni_syscall)

    .endr

    3.) 把增長的 sys_call_table 表項所對應的向量,在include/asm-386/unistd.h 中進行必要申明,以供用戶進程和其餘系統進程查詢或調用:

    增長後的部分 /usr/src/linux/include/asm-386/unistd.h 文件以下:

    ... ...

    #define __NR_sendfile 187

    #define __NR_getpmsg 188

    #define __NR_putpmsg 189

    #define __NR_vfork 190

    /* add by I */

    #define __NR_addtotal 191

    4.測試程序(test.c)以下:

    #include

    #include

    _syscall1(int,addtotal,int, num)

    main()

    {

    int i,j;

    do

    printf("Please input a numbern");

    while(scanf("%d",&i)==EOF);

    if((j=addtotal(i))==-1)

    printf("Error occurred in syscall-addtotal();n");

    printf("Total from 0 to %d is %d n",i,j);

    }

      對修改後的新的內核進行編譯,並引導它做爲新的操做系統,運行幾個程序後能夠發現一切正常;在新的系統下對測試程序進行編譯(*注:因爲原內核並未提供此係統調用,因此只有在編譯後的新內核下,此測試程序才能可能被編譯經過),運行狀況以下:

    $gcc &#0;o test test.c

    $./test

    Please input a number

    36

    Total from 0 to 36 is 666

    綜述

      可見,修改爲功。

      因爲操做系統內核源碼的特殊性:體系龐大,結構複雜,代碼冗長,代碼間聯繫錯綜複雜。因此要把內核源碼分析清楚,也是一個很艱難,很須要毅力的事。尤爲須要交流和講究方法;只有方法正確,才能事半功倍。

      在上面的論述中,一共列舉了兩個內核分析的入口、和三種分析源碼的方法:以程序流程爲線索,一線串珠;以數據結構爲基點,舉一反三;以功能爲中心,各個擊破。三種方法各有特色,適合於分析不一樣部分的代碼:

  • 以數據結構爲基點、舉一反三,這種方法是分析操做系統源碼最經常使用的和最主要的方法。對分析進程管理,設備管理,內存管理等等都是頗有效的。
  • 以功能爲中心、各個擊破,是把整個系統分紅幾個相對獨立的功能模塊,而後分別對各個功能進行分析。這樣帶來的一個好處就是,每次只以一 個功能爲中心,涉及到其餘部分的內容,能夠看做是其它功能提供的服務,而無需急着追究這種服務的實現細節;這樣,在很大程度上減輕了分析的複雜度。

      三種方法,各有其長,只要合理的綜合運用這些方法,相信對減輕分析的複雜度仍是有所幫組的。

 

 

 

LINUX中斷機制

【主要內容】

Linux設備驅動編程中的中斷與定時器處理

【正文】

1、基礎知識

一、中斷

所謂中斷是指CPU在執行程序的過程當中,出現了某些突發事件急待處理,CPU必須暫停執行當前的程序,轉去處理突發事件,處理完畢後CPU又返回程序被中斷的位置並繼續執行。

二、中斷的分類

  1)根據中斷來源分爲:內部中斷和外部中斷。內部中斷來源於CPU內部(軟中斷指令、溢出、語法錯誤等),外部中斷來自CPU外部,由設備提出請求。

  2)根據是否可被屏蔽分爲:可屏蔽中斷和不可屏蔽中斷(NMI),被屏蔽的中斷將不會獲得響應。

  3)根據中斷入口跳轉方法分爲:向量中斷和非向量中斷。向量中斷爲不一樣的中斷分配不一樣的中斷號,非向量中斷多箇中斷共享一箇中斷號,在軟件中判斷具體是哪一個中斷(非向量中斷由軟件提供中斷服務程序入口地址)。

2、Linux中斷處理程序架構

設備的中斷會打斷內核中正常調度和運行,系統對更高吞吐率的追求勢必要求中斷服務程序儘量的短小(時間短),可是在大多數實際使用中,要完成的工做都是複雜的,它可能須要進行大量的耗時工做。

一、Linux中斷處理中的頂半部和底半部機制

因爲中斷服務程序的執行並不存在於進程上下文,所以,要求中斷服務程序的時間儘量的短。 爲了在中斷執行事件儘量短和中斷處理需完成大量耗時工做之間找到一個平衡點,Linux將中斷處理分爲兩個部分:頂半部(top half)和底半部(bottom half)。

Linux中斷處理機制

頂半部完成儘量少的比較緊急的功能,它每每只是簡單地讀取寄存器中的中斷狀態並清除中斷標誌後進行「登記中斷」的工做。「登記」意味着將底半部的處理程序掛載到該設備的底半部指向隊列中去。底半部做爲工做重心,完成中斷事件的絕大多數任務。

a. 底半部能夠被新的中斷事件打斷,這是和頂半部最大的不一樣,頂半部一般被設計成不可被打斷

b. 底半部相對來講不是很是緊急的,並且相對比較耗時,不在硬件中斷服務程序中執行。

c. 若是中斷要處理的工做自己不多,全部的工做可在頂半部所有完成

3、中斷編程

一、申請和釋放中斷

在Linux設備驅動中,使用中斷的設備須要申請和釋放相對應的中斷,分別使用內核提供的 request_irq() 和 free_irq() 函數

a. 申請IRQ

typedef irqreturn_t (*irq_handler_t)(int irq, void *dev_id);

 

複製代碼
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long irqflags, const char *devname, void *dev_id) /* 參數: ** irq:要申請的硬件中斷號 ** handler:中斷處理函數(頂半部) ** irqflags:觸發方式及工做方式 **      觸發:IRQF_TRIGGER_RISING  上升沿觸發 **      IRQF_TRIGGER_FALLING  降低沿觸發 **      IRQF_TRIGGER_HIGH  高電平觸發 **      IRQF_TRIGGER_LOW  低電平觸發 **      工做:不寫:快速中斷(一個設備佔用,且中斷例程回調過程當中會屏蔽中斷) **      IRQF_SHARED:共享中斷 ** dev_id:在共享中斷時會用到(中斷註銷與中斷註冊的此參數應保持一致) ** 返回值:成功返回 - 0      失敗返回 - 負值(絕對值爲錯誤碼) */
複製代碼

b. 釋放IRQ

void free_irq(unsigned int irq, void *dev_id); /* 參數參見申請IRQ */

二、屏蔽和使能中斷

void disable_irq(int irq);  //屏蔽中短、當即返回 void disable_irq_nosync(int irq);  //屏蔽中斷、等待當前中斷處理結束後返回 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ void enable_irq(int irq);  //使能中斷

 全局中斷使能和屏蔽函數(或宏)

屏蔽:

#define local_irq_save(flags) ...
void local irq_disable(void );

 使能:

#define local_irq_restore(flags) ...
void local_irq_enable(void);

三、底半部機制

Linux實現底半部機制的的主要方式有 Tasklet、工做隊列和軟中斷

a. Tasklet

Tasklet使用簡單,只須要定義tasklet及其處理函數並將兩者關聯便可,例如:

void my_tasklet_func(unsigned long);  /* 定義一個處理函數 */ DECLARE_TASKLET(my_tasklet, my_tasklet_func, data);
/* 定義一個名爲 my_tasklet 的 struct tasklet 並將其與 my_tasklet_func 綁定,data爲傳入 my_tasklet_func的參數 */

只須要在頂半部中電泳 tasklet_schedule()函數就能使系統在適當的時候進行調度運行

tasklet_schedule(struct tasklet *xxx_tasklet);

tasklet使用模版

複製代碼
/* 定義 tasklet 和底半部函數並關聯 */ void xxx_do_tasklet(unsigned long data); DECLARE_TASKLET(xxx_tasklet, xxx_tasklet_func, data); /* 中斷處理底半部 */ void xxx_tasklet_func() { /* 中斷處理具體操做 */ } /* 中斷處理頂半部 */ irqreturn xxx_interrupt(int irq, void *dev_id) { //do something task_schedule(&xxx_tasklet); //do something
   return IRQ_HANDLED; } /* 設備驅動模塊 init */ int __init xxx_init(void) { ... /* 申請設備中斷 */ result = request_irq(xxx_irq, xxx_interrupt, IRQF_DISABLED, "xxx", NULL); ... return 0; } module_init(xxx_init); /* 設備驅動模塊exit */ void __exit xxx_exit(void) { ... /* 釋放中斷 */ free_irq(xxx_irq, NULL); }
module_exit(xxx_exit);
複製代碼

b. 工做隊列 workqueue

工做隊列與tasklet方法很是相似,使用一個結構體定義一個工做隊列和一個底半部執行函數:

複製代碼
struct work_struct {   atomic_long_t data;   struct list_head entry;   work_func_t func; #ifdef CONFIG_LOCKDEP   struct lockdep_map lockdep_map; #endif };
複製代碼
struct work_struct my_wq; /* 定義一個工做隊列 */ void my_wq_func(unsigned long); /*定義一個處理函數 */

經過INIT_WORK()能夠初始化這個工做隊列並將工做隊列與處理函數綁定(通常在模塊初始化中使用):

void INIT_WORK(struct work_struct *my_wq, work_func_t); /* my_wq 工做隊列地址 ** work_func_t 處理函數 */

與tasklet_schedule_work ()對應的用於調度工做隊列執行的函數爲schedule_work()

schedule_work(&my_wq);

工做隊列使用模版

複製代碼
/* 定義工做隊列和關聯函數 */ struct work_struct xxx_wq; void xxx_do_work(unsigned long); /* 中斷處理底半部 */ void xxx_work(unsigned long) {   /* do something */ } /* 中斷處理頂半部 */ irqreturn_t xxx_interrupt(int irq, void *dev_id) {   ...   schedule_work(&xxx_wq);   ...   return IRQ_HANDLED; } /* 設備驅動模塊 init */ int __init xxx_init(void) { ... /* 申請設備中斷 */ result = request_irq(xxx_irq, xxx_interrupt, IRQF_DISABLED, "xxx", NULL);   /* 初始化工做隊列 */   INIT_WORK(&xxx_wq, xxx_do_work); ... return 0; } module_init(xxx_init); /* 設備驅動模塊exit */ void __exit xxx_exit(void) { ... /* 釋放中斷 */ free_irq(xxx_irq, NULL); } module_exit(xxx_exit);
複製代碼

c. 軟中斷

軟中斷(softirq)也是一種傳統的底半部處理機制,它的執行時機一般是頂半部返回的時候,tasklet的基於軟中斷實現的,所以也運行於軟中斷上下文。

在Linux內核中,用softirq_action結構體表徵一個軟中斷,這個結構體中包含軟中斷處理函數指針和傳遞給該函數的參數。使用open_softirq()函數能夠註冊軟中斷對應的處理函數,而raise_softirq()函數能夠觸發一個軟中斷。

struct softirq_action {   void (*action)(struct softirq_action *); };
void open_softirq(int nr, void (*action)(struct softirq_action *));  /* 註冊軟中斷 */ void raise_softirq(unsigned int nr);  /* 觸發軟中斷 */

 local_bh_disable() 和 local_bh_enable() 是內核中用於禁止和使能軟中斷和tasklet底半部機制的函數。

 

 

 

 

linux中斷處理原理分析

Tasklet做爲一種新機制,顯然能夠承擔更多的優勢。正好這時候SMP愈來愈火了,所以又在tasklet中加入了SMP機制,保證同種中斷只能在一個cpu上執行。在軟中斷時代,顯然沒有這種考慮。所以同一種中斷能夠在兩個cpu上同時執行,極可能形成衝突。

Linux中斷下半部處理有三種方式:軟中斷、tasklet、工做隊列。

曾經有人問我爲何要分這幾種,該怎麼用。當時用書上的東西蒙混了過去,可是本身明白本身其實是不懂的。最近有時間了,因而試着整理一下linux的中斷處理機制,目的是起碼從原理上可以說得通。

1、最簡單的中斷機制

最簡單的中斷機制就是像芯片手冊上講的那樣,在中斷向量表中填入跳轉到對應處理函數的指令,而後在處理函數中實現須要的功能。相似下圖:

 

這種方式在原來的單片機課程中經常用到,一些簡單的單片機系統也是這樣用。

它的好處很明顯,簡單,直接。

 

2、下半部

中斷處理函數所做的第一件事情是什麼?答案是屏蔽中斷(或者是什麼都不作,由於經常是若是不清除IF位,就等於屏蔽中斷了),固然只屏蔽同一種中斷。之因此要屏蔽中斷,是由於新的中斷會再次調用中斷處理函數,致使原來中斷處理現場的破壞。即,破壞了 interrupt context。

隨着系統的不斷複雜,中斷處理函數要作的事情也愈來愈多,多到都來不及接收新的中斷了。因而發生了中斷丟失,這顯然不行,因而產生了新的機制:分離中斷接收與中斷處理過程。中斷接收在屏蔽中斷的狀況下完成;中斷處理在時能中斷的狀況下完成,這部分被稱爲中斷下半部。

 

從上圖中看,只看int0的處理。Func0爲中斷接收函數。中斷只能簡單的觸發func0,而func0則能作更多的事情,它與funcA之間能夠使用隊列等緩存機制。當又有中斷髮生時,func0被觸發,而後發送一箇中斷請求到緩存隊列,而後讓funcA去處理。

因爲func0作的事情是很簡單的,因此不會影響int0的再次接收。並且在func0返回時就會使能int0,所以funcA執行時間再長也不會影響int0的接收。

 

3、軟中斷

下面看看linux中斷處理。做爲一個操做系統顯然不能任由每一箇中斷都各自爲政,統一管理是必須的。

咱們不可中斷部分的共同部分放在函數do_IRQ中,須要添加中斷處理函數時,經過request_irq實現。下半部放在do_softirq中,也就是軟中斷,經過open_softirq添加對應的處理函數。

 

 

4、tasklet

舊事物跟不上歷史的發展時,總會有新事物出現。

隨着中斷數的不停增長,軟中斷不夠用了,因而下半部又作了進化。

軟中斷用輪詢的方式處理。假如正好是最後一種中斷,則必須循環完全部的中斷類型,才能最終執行對應的處理函數。顯然當年開發人員爲了保證輪詢的效率,因而限制中斷個數爲32個。

爲了提升中斷處理數量,順道改進處理效率,因而產生了tasklet機制。

Tasklet採用無差異的隊列機制,有中斷時才執行,免去了循環查表之苦。

 

總結下tasklet的優勢:

(1)無類型數量限制;

(2)效率高,無需循環查表;

(3)支持SMP機制;

 

5、工做隊列

前面的機制不論如何折騰,有一點是不會變的。它們都在中斷上下文中。什麼意思?說明它們不可掛起。並且因爲是串行執行,所以只要有一個處理時間較長,則會致使其餘中斷響應的延遲。爲了完成這些不可能完成的任務,因而出現了工做隊列。工做隊列說白了就是一組內核線程,做爲中斷守護線程來使用。多箇中斷能夠放在一個線程中,也能夠每一箇中斷分配一個線程。

工做隊列對線程做了封裝,使用起來更方便。

由於工做隊列是線程,因此咱們能夠使用全部能夠在線程中使用的方法。

 

Tasklet其實也不必定是在中斷上下文中執行,它也有可能在線程中執行。

假如中斷數量不少,並且這些中斷都是自啓動型的(中斷處理函數會致使新的中斷產生),則有可能cpu一直在這裏執行中斷處理函數,會致使用戶進程永遠得不到調度時間。

爲了不這種狀況,linux發現中斷數量過多時,會把多餘的中斷處理放到一個單獨的線程中去作,就是ksoftirqd線程。這樣又保證了中斷很少時的響應速度,又保證了中斷過多時不會把用戶進程餓死。

問題是咱們不能保證咱們的tasklet或軟中斷處理函數必定會在線程中執行,因此仍是不能使用進程才能用的一些方法,如放棄調度、長延時等。

 

6、使用方式總結

Request_irq掛的中斷函數要儘可能簡單,只作必須在屏蔽中斷狀況下要作的事情。

中斷的其餘部分都在下半部中完成。

軟中斷的使用原則很簡單,永遠不用。它甚至都不算是一種正是的中斷處理機制,而只是tasklet的實現基礎。

工做隊列也要少用,若是不是必需要用到線程才能用的某些機制,就不要使用工做隊列。其實對於中斷來講,只是對中斷進行簡單的處理,大部分工做是在驅動程序中完成的。因此有什麼必要非使用工做隊列呢?

除了上述狀況,就要使用tasklet。

即便是下半部,也只是做必須在中斷中要作的事情,如保存數據等,其餘都交給驅動程序去作。

 

 

 

 

linux 中斷機制的處理過程

1、中斷的概念

中斷是指在CPU正常運行期間,因爲內外部事件或由程序預先安排的事件引發的CPU暫時中止正在運行的程序,轉而爲該內部或外部事件或預先安排的事件服務的程序中去,服務完畢後再返回去繼續運行被暫時中斷的程序。Linux中一般分爲外部中斷(又叫硬件中斷)和內部中斷(又叫異常)。

在實地址模式中,CPU把內存中從0開始的1KB空間做爲一箇中斷向量表。表中的每一項佔4個字節。可是在保護模式中,有這4個字節的表項構成的中斷向量表不知足實際需求,因而根據反映模式切換的信息和偏移量的足夠使得中斷向量表的表項由8個字節組成,而中斷向量表也叫作了中斷描述符表(IDT)。在CPU中增長了一個用來描述中斷描述符表寄存器(IDTR),用來保存中斷描述符表的起始地址。

2、中斷的請求過程

外部設備當須要操做系統作相關的事情的時候,會產生相應的中斷。設備經過相應的中斷線向中斷控制器發送高電平以產生中斷信號,而操做系統則會從中斷控制器的狀態位取得那根中斷線上產生的中斷。並且只有在設備在對某一條中斷線擁有控制權,才能夠向這條中斷線上發送信號。也因爲如今的外設愈來愈多,中斷線又是很寶貴的資源不可能被一一對應。所以在使用中斷線前,就得對相應的中斷線進行申請。不管採用共享中斷方式仍是獨佔一箇中斷,申請過程都是先講全部的中斷線進行掃描,得出哪些沒有別佔用,從其中選擇一個做爲該設備的IRQ。其次,經過中斷申請函數申請相應的IRQ。最後,根據申請結果查看中斷是否可以被執行。

中斷機制的核心數據結構是 irq_desc, 它完整地描述了一條中斷線 (或稱爲 「中斷通道」 )。如下程序源碼版本爲linux-2.6.32.2。

其中irq_desc 結構在 include/linux/irq.h 中定義:

typedef    void (*irq_flow_handler_t)(unsigned int irq,

                      struct irq_desc *desc);

struct irq_desc {

    unsigned int      irq;    

    struct timer_rand_state *timer_rand_state;

    unsigned int            *kstat_irqs;

#ifdef CONFIG_INTR_REMAP

    struct irq_2_iommu      *irq_2_iommu;

#endif

    irq_flow_handler_t   handle_irq; /* 高層次的中斷事件處理函數 */

    struct irq_chip      *chip; /* 低層次的硬件操做 */

    struct msi_desc      *msi_desc;

    void          *handler_data; /* chip 方法使用的數據*/

    void          *chip_data; /* chip 私有數據 */

    struct irqaction  *action;   /* 行爲鏈表(action list) */

    unsigned int      status;       /* 狀態 */

    unsigned int      depth;     /* 關中斷次數 */

    unsigned int      wake_depth;   /* 喚醒次數 */

    unsigned int      irq_count; /* 發生的中斷次數 */

    unsigned long     last_unhandled;   /*滯留時間 */

    unsigned int      irqs_unhandled;

    spinlock_t    lock; /*自選鎖*/

#ifdef CONFIG_SMP

    cpumask_var_t     affinity;

    unsigned int      node;

#ifdef CONFIG_GENERIC_PENDING_IRQ

    cpumask_var_t     pending_mask;

#endif

#endif

    atomic_t      threads_active;

    wait_queue_head_t   wait_for_threads;

#ifdef CONFIG_PROC_FS

    struct proc_dir_entry    *dir; /* 在 proc 文件系統中的目錄 */

#endif

    const char    *name;/*名稱*/

} ____cacheline_internodealigned_in_smp;

 

I、Linux中斷的申請與釋放:在<linux/interrupt.h>, , 實現中斷申請接口:

request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,const char *name, void *dev);

函數參數說明

unsigned int irq:所要申請的硬件中斷號

irq_handler_t handler:中斷服務程序的入口地址,中斷髮生時,系統調用handler這個函數。irq_handler_t爲自定義類型,其原型爲:

typedef irqreturn_t (*irq_handler_t)(int, void *);

而irqreturn_t的原型爲:typedef enum irqreturn irqreturn_t;

enum irqreturn {

    IRQ_NONE,/*此設備沒有產生中斷*/

    IRQ_HANDLED,/*中斷被處理*/

    IRQ_WAKE_THREAD,/*喚醒中斷*/

};

在枚舉類型irqreturn定義在include/linux/irqreturn.h文件中。

 

unsigned long flags:中斷處理的屬性,與中斷管理有關的位掩碼選項,有一下幾組值:

#define IRQF_DISABLED       0x00000020    /*中斷禁止*/

#define IRQF_SAMPLE_RANDOM  0x00000040    /*供系統產生隨機數使用*/

#define IRQF_SHARED      0x00000080 /*在設備之間可共享*/

#define IRQF_PROBE_SHARED   0x00000100/*探測共享中斷*/

#define IRQF_TIMER       0x00000200/*專用於時鐘中斷*/

#define IRQF_PERCPU      0x00000400/*每CPU週期執行中斷*/

#define IRQF_NOBALANCING 0x00000800/*復位中斷*/

#define IRQF_IRQPOLL     0x00001000/*共享中斷中根據註冊時間判斷*/

#define IRQF_ONESHOT     0x00002000/*硬件中斷處理完後觸發*/

#define IRQF_TRIGGER_NONE   0x00000000/*無觸發中斷*/

#define IRQF_TRIGGER_RISING 0x00000001/*指定中斷觸發類型:上升沿有效*/

#define IRQF_TRIGGER_FALLING 0x00000002/*中斷觸發類型:降低沿有效*/

#define IRQF_TRIGGER_HIGH   0x00000004/*指定中斷觸發類型:高電平有效*/

#define IRQF_TRIGGER_LOW 0x00000008/*指定中斷觸發類型:低電平有效*/

#define IRQF_TRIGGER_MASK   (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | /

               IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)

#define IRQF_TRIGGER_PROBE  0x00000010/*觸發式檢測中斷*/

 

const char *dev_name:設備描述,表示那一個設備在使用這個中斷。

 

void *dev_id:用做共享中斷線的指針.。通常設置爲這個設備的設備結構體或者NULL。它是一個獨特的標識, 用在當釋放中斷線時以及可能還被驅動用來指向它本身的私有數據區,來標識哪一個設備在中斷 。這個參數在真正的驅動程序中通常是指向設備數據結構的指針.在調用中斷處理程序的時候它就會傳遞給中斷處理程序的void *dev_id。若是中斷沒有被共享, dev_id 能夠設置爲 NULL。

II、釋放IRQ

void free_irq(unsigned int irq, void *dev_id);

III、中斷線共享的數據結構

   struct irqaction {

    irq_handler_t handler; /* 具體的中斷處理程序 */

    unsigned long flags;/*中斷處理屬性*/

    const char *name; /* 名稱,會顯示在/proc/interreupts 中 */

    void *dev_id; /* 設備ID,用於區分共享一條中斷線的多個處理程序 */

    struct irqaction *next; /* 指向下一個irq_action 結構 */

    int irq;  /* 中斷通道號 */

    struct proc_dir_entry *dir; /* 指向proc/irq/NN/name 的入口*/

    irq_handler_t thread_fn;/*線程中斷處理函數*/

    struct task_struct *thread;/*線程中斷指針*/

    unsigned long thread_flags;/*與線程有關的中斷標記屬性*/

};

thread_flags參見枚舉型

enum {

    IRQTF_RUNTHREAD,/*線程中斷處理*/

    IRQTF_DIED,/*線程中斷死亡*/

    IRQTF_WARNED,/*警告信息*/

    IRQTF_AFFINITY,/*調整線程中斷的關係*/

};

多箇中斷處理程序能夠共享同一條中斷線,irqaction 結構中的 next 成員用來把共享同一條中斷線的全部中斷處理程序組成一個單向鏈表,dev_id 成員用於區分各個中斷處理程序。

根據以上內容能夠得出中斷機制各個數據結構之間的聯繫以下圖所示:
linux 中斷機制的處理過程 - 枯木青花 - 枯木青花
 三.中斷的處理過程

Linux中斷分爲兩個半部:上半部(tophalf)和下半部(bottom half)。上半部的功能是"登記中斷",當一箇中斷髮生時,它進行相應地硬件讀寫後就把中斷例程的下半部掛到該設備的下半部執行隊列中去。所以,上半部執行的速度就會很快,能夠服務更多的中斷請求。可是,僅有"登記中斷"是遠遠不夠的,由於中斷的事件可能很複雜。所以,Linux引入了一個下半部,來完成中斷事件的絕大多數使命。下半部和上半部最大的不一樣是下半部是可中斷的,而上半部是不可中斷的,下半部幾乎作了中斷處理程序全部的事情,並且能夠被新的中斷打斷!下半部則相對來講並非很是緊急的,一般仍是比較耗時的,所以由系統自行安排運行時機,不在中斷服務上下文中執行。

中斷號的查看能夠使用下面的命令:「cat /proc/interrupts」。

Linux實現下半部的機制主要有tasklet和工做隊列。

小任務tasklet的實現

其數據結構爲struct tasklet_struct,每個結構體表明一個獨立的小任務,定義以下

 

struct tasklet_struct

{

    struct tasklet_struct *next;/*指向下一個鏈表結構*/

    unsigned long state;/*小任務狀態*/

    atomic_t count;/*引用計數器*/

    void (*func)(unsigned long);/*小任務的處理函數*/

    unsigned long data;/*傳遞小任務函數的參數*/

};

 

state的取值參照下邊的枚舉型:

enum

{

    TASKLET_STATE_SCHED,    /* 小任務已被調用執行*/

    TASKLET_STATE_RUN   /*僅在多處理器上使用*/

};

count域是小任務的引用計數器。只有當它的值爲0的時候才能被激活,並其被設置爲掛起狀態時,纔可以被執行,不然爲禁止狀態。

I、聲明和使用小任務tasklet

靜態的建立一個小任務的宏有一下兩個:

#define DECLARE_TASKLET(name, func, data)  /

struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }

 

#define DECLARE_TASKLET_DISABLED(name, func, data) /

struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }

這兩個宏的區別在於計數器設置的初始值不一樣,前者能夠看出爲0,後者爲1。爲0的表示激活狀態,爲1的表示禁止狀態。其中ATOMIC_INIT宏爲:

#define ATOMIC_INIT(i)   { (i) }

便可看出就是設置的數字。此宏在include/asm-generic/atomic.h中定義。這樣就建立了一個名爲name的小任務,其處理函數爲func。當該函數被調用的時候,data參數就被傳遞給它。

II、小任務處理函數程序

    處理函數的的形式爲:void my_tasklet_func(unsigned long data)。這樣DECLARE_TASKLET(my_tasklet, my_tasklet_func, data)實現了小任務名和處理函數的綁定,而data就是函數參數。

III、調度編寫的tasklet

調度小任務時引用tasklet_schedule(&my_tasklet)函數就能使系統在合適的時候進行調度。函數原型爲:

static inline void tasklet_schedule(struct tasklet_struct *t)

{

    if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))

       __tasklet_schedule(t);

}

這個調度函數放在中斷處理的上半部處理函數中,這樣中斷申請的時候調用處理函數(即irq_handler_t handler)後,轉去執行下半部的小任務。

若是但願使用DECLARE_TASKLET_DISABLED(name,function,data)建立小任務,那麼在激活的時候也得調用相應的函數被使能

tasklet_enable(struct tasklet_struct *); //使能tasklet

tasklet_disble(struct tasklet_struct *); //禁用tasklet

tasklet_init(struct tasklet_struct *,void (*func)(unsigned long),unsigned long);

固然也能夠調用tasklet_kill(struct tasklet_struct *)從掛起隊列中刪除一個小任務。清除指定tasklet的可調度位,即不容許調度該tasklet 。

使用tasklet做爲下半部的處理中斷的設備驅動程序模板以下:

/*定義tasklet和下半部函數並關聯*/

void my_do_tasklet(unsigned long);

DECLARE_TASKLET(my_tasklet, my_tasklet_func, 0);

/*中斷處理下半部*/

void my_do_tasklet(unsigned long)

{

  ……/*編寫本身的處理事件內容*/

}

/*中斷處理上半部*/

irpreturn_t my_interrupt(unsigned int irq,void *dev_id)

{

 ……

 tasklet_schedule(&my_tasklet)/*調度my_tasklet函數,根據聲明將去執行my_tasklet_func函數*/

 ……

}

/*設備驅動的加載函數*/

int __init xxx_init(void)

{

 ……

 /*申請中斷, 轉去執行my_interrupt函數並傳入參數*/

 result=request_irq(my_irq,my_interrupt,IRQF_DISABLED,"xxx",NULL);

 ……

}

/*設備驅動模塊的卸載函數*/

void __exit xxx_exit(void)

{

……

/*釋放中斷*/

free_irq(my_irq,my_interrupt);

……

}

工做隊列的實現

工做隊列work_struct結構體,位於/include/linux/workqueue.h

 

typedef void (*work_func_t)(struct work_struct *work);

struct work_struct {

      atomic_long_t data; /*傳遞給處理函數的參數*/

#define WORK_STRUCT_PENDING 0/*這個工做是否正在等待處理標誌*/             

#define WORK_STRUCT_FLAG_MASK (3UL)

#define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)

      struct list_head entry;  /* 鏈接全部工做的鏈表*/

      work_func_t func; /* 要執行的函數*/

#ifdef CONFIG_LOCKDEP

      struct lockdep_map lockdep_map;

#endif

};

這些結構被鏈接成鏈表。當一個工做者線程被喚醒時,它會執行它的鏈表上的全部工做。工做被執行完畢,它就將相應的work_struct對象從鏈表上移去。當鏈表上再也不有對象的時候,它就會繼續休眠。能夠經過DECLARE_WORK在編譯時靜態地建立該結構,以完成推後的工做。

#define DECLARE_WORK(n, f)                                 /

      struct work_struct n = __WORK_INITIALIZER(n, f)

然後邊這個宏爲一下內容:

#define __WORK_INITIALIZER(n, f) {                      /

      .data = WORK_DATA_INIT(),                            /

      .entry      = { &(n).entry, &(n).entry },                    /

      .func = (f),                                        /

      __WORK_INIT_LOCKDEP_MAP(#n, &(n))                   /

      }

其爲參數data賦值的宏定義爲:

#define WORK_DATA_INIT()       ATOMIC_LONG_INIT(0)

這樣就會靜態地建立一個名爲n,待執行函數爲f,參數爲data的work_struct結構。一樣,也能夠在運行時經過指針建立一個工做:

INIT_WORK(struct work_struct *work, void(*func) (void *));

這會動態地初始化一個由work指向的工做隊列,並將其與處理函數綁定。宏原型爲:

#define INIT_WORK(_work, _func)                                        /

      do {                                                        /

             static struct lock_class_key __key;                 /

                                                              /

             (_work)->data = (atomic_long_t) WORK_DATA_INIT();  /

             lockdep_init_map(&(_work)->lockdep_map, #_work, &__key, 0);/

             INIT_LIST_HEAD(&(_work)->entry);                 /

             PREPARE_WORK((_work), (_func));                         /

      } while (0)

在須要調度的時候引用相似tasklet_schedule()函數的相應調度工做隊列執行的函數schedule_work(),如:

schedule_work(&work);/*調度工做隊列執行*/

若是有時候並不但願工做立刻就被執行,而是但願它通過一段延遲之後再執行。在這種狀況下,能夠調度指定的時間後執行函數:

schedule_delayed_work(&work,delay);函數原型爲:

int schedule_delayed_work(struct delayed_work *work, unsigned long delay);

其中是以delayed_work爲結構體的指針,而這個結構體的定義是在work_struct結構體的基礎上增長了一項timer_list結構體。

struct delayed_work {

    struct work_struct work;

    struct timer_list timer; /* 延遲的工做隊列所用到的定時器,當不須要延遲時初始化爲NULL*/

};

這樣,便使預設的工做隊列直到delay指定的時鐘節拍用完之後纔會執行。

使用工做隊列處理中斷下半部的設備驅動程序模板以下:

/*定義工做隊列和下半部函數並關聯*/

struct work_struct my_wq;

void my_do_work(unsigned long);

/*中斷處理下半部*/

void my_do_work(unsigned long)

{

  ……/*編寫本身的處理事件內容*/

}

/*中斷處理上半部*/

irpreturn_t my_interrupt(unsigned int irq,void *dev_id)

{

 ……

 schedule_work(&my_wq)/*調度my_wq函數,根據工做隊列初始化函數將去執行my_do_work函數*/

 ……

}

/*設備驅動的加載函數*/

int __init xxx_init(void)

{

 ……

 /*申請中斷,轉去執行my_interrupt函數並傳入參數*/

 result=request_irq(my_irq,my_interrupt,IRQF_DISABLED,"xxx",NULL);

 ……

 /*初始化工做隊列函數,並與自定義處理函數關聯*/

 INIT_WORK(&my_irq,(void (*)(void *))my_do_work);

 ……

}

/*設備驅動模塊的卸載函數*/

void __exit xxx_exit(void)

{

……

/*釋放中斷*/

free_irq(my_irq,my_interrupt);

……

}

 

 

 

 

深刻剖析Linux中斷機制之三---Linux對異常和中斷的處理

【摘要】本文詳解了Linux內核的中斷實現機制。首先介紹了中斷的一些基本概念,而後分析了面向對象的Linux中斷的組織形式、三種主要數據結構及其之間的關係。隨後介紹了Linux處理異常和中斷的基本流程,在此基礎上分析了中斷處理的詳細流程,包括保存現場、中斷處理、中斷退出時的軟中斷執行及中斷返回時的進程切換等問題。最後介紹了中斷相關的API,包括中斷註冊和釋放、中斷關閉和使能、如何編寫中斷ISR、共享中斷、中斷上下文中斷狀態等。

【關鍵字】中斷,異常,hw_interrupt_type,irq_desc_t,irqaction,asm_do_IRQ,軟中斷,進程切換,中斷註冊釋放request_irq,free_irq,共享中斷,可重入,中斷上下文

 

 

1       Linux對異常和中斷的處理

1.1    異常處理

Linux利用異常來達到兩個大相徑庭的目的:

²      給進程發送一個信號以通報一個反常狀況

²      管理硬件資源

 

對於第一種狀況,例如,若是進程執行了一個被0除的操做,CPU則會產生一個「除法錯誤」異常,並由相應的異常處理程序向當前進程發送一個SIGFPE信號。當前進程接收到這個信號後,就要採起若干必要的步驟,或者從錯誤中恢復,或者終止執行(若是這個信號沒有相應的信號處理程序)。

 

內核對異常處理程序的調用有一個標準的結構,它由如下三部分組成:

²      在內核棧中保存大多數寄存器的內容(由彙編語言實現)

²      調用C編寫的異常處理函數

²      經過ret_from_exception()函數從異常退出。

 

1.2    中斷處理

當一箇中斷髮生時,並非全部的操做都具備相同的急迫性。事實上,把全部的操做都放進中斷處理程序自己並不合適。須要時間長的、非重要的操做應該推後,由於當一箇中斷處理程序正在運行時,相應的IRQ中斷線上再發出的信號就會被忽略。另外中斷處理程序不能執行任何阻塞過程,如I/O設備操做。所以,Linux把一箇中斷要執行的操做分爲下面的三類:

²      緊急的(Critical)

這樣的操做諸如:中斷到來時中斷控制器作出應答,對中斷控制器或設備控制器從新編程,或者對設備和處理器同時訪問的數據結構進行修改。這些操做都是緊急的,應該被很快地執行,也就是說,緊急操做應該在一箇中斷處理程序內當即執行,並且是在禁用中斷的狀態下。

²      非緊急的(Noncritical)

這樣的操做如修改那些只有處理器纔會訪問的數據結構(例如,按下一個鍵後,讀掃描碼)。這些操做也要很快地完成,所以,它們由中斷處理程序當即執行,但在啓用中斷的狀態下。

²      非緊急可延遲的(Noncritical deferrable)

這樣的操做如,把一個緩衝區的內容拷貝到一些進程的地址空間(例如,把鍵盤行緩衝區的內容發送到終端處理程序的進程)。這些操做可能被延遲較長的時間間隔而不影響內核操做,有興趣的進程會等待須要的數據。

 

全部的中斷處理程序都執行四個基本的操做:

²      在內核棧中保存IRQ的值和寄存器的內容。

²      給與IRQ中斷線相連的中斷控制器發送一個應答,這將容許在這條中斷線上進一步發出中斷請求。

²      執行共享這個IRQ的全部設備的中斷服務例程(ISR)。

²      跳到ret_to_usr( )的地址後終止。

 

1.3    中斷處理程序的執行流程

1.3.1      流程概述

如今,咱們能夠從中斷請求的發生到CPU的響應,再到中斷處理程序的調用和返回,沿着這一思路走一遍,以體會Linux內核對中斷的響應及處理。

 

假定外設的驅動程序都已完成了初始化工做,而且已把相應的中斷服務例程掛入到特定的中斷請求隊列。又假定當前進程正在用戶空間運行(隨時能夠接受中斷),且外設已產生了一次中斷請求,CPU就在執行完當前指令後來響應該中斷。

 

中斷處理系統在Linux中的實現是很是依賴於體系結構的,實現依賴於處理器、所使用的中斷控制器的類型、體系結構的設計及機器自己。

 

設備產生中斷,經過總線把電信號發送給中斷控制器。若是中斷線是激活的,那麼中斷控制器就會把中斷髮往處理器。在大多數體系結構中,這個工做就是經過電信號給處理器的特定管腳發送一個信號。除非在處理器上禁止該中斷,不然,處理器會當即中止它正在作的事,關閉中斷系統,而後跳到內存中預約義的位置開始執行那裏的代碼。這個預約義的位置是由內核設置的,是中斷處理程序的入口點。

 

對於ARM系統來講,有個專用的IRQ運行模式,有一個統一的入口地址。假定中斷髮生時CPU運行在用戶空間,而中斷處理程序屬於內核空間,所以,要進行堆棧的切換。也就是說,CPU從TSS中取出內核棧指針,並切換到內核棧(此時棧還爲空)。

 

 

 

若當前處於內核空間時,對於ARM系統來講是處於SVC模式,此時產生中斷,中斷處理完畢後,如果可剝奪內核,則檢查是否須要進行進程調度,不然直接返回到被中斷的內核空間;若須要進行進程調度,則svc_preempt,進程切換。

 190        .align  5

 191__irq_svc:

 192        svc_entry

 197#ifdef CONFIG_PREEMPT

 198        get_thread_info tsk

 199        ldr     r8, [tsk, #TI_PREEMPT]          @ get preempt count

 200        add     r7, r8, #1                      @ increment it

 201        str     r7, [tsk, #TI_PREEMPT]

 202#endif

 203

 204        irq_handler

 205#ifdef CONFIG_PREEMPT

 206        ldr     r0, [tsk, #TI_FLAGS]            @ get flags

 207        tst     r0, #_TIF_NEED_RESCHED

 208        blne    svc_preempt

 209preempt_return:

 210        ldr     r0, [tsk, #TI_PREEMPT]          @ read preempt value

 211        str     r8, [tsk, #TI_PREEMPT]          @ restore preempt count

 212        teq     r0, r7

 213        strne   r0, [r0, -r0]                   @ bug()

 214#endif

 215        ldr     r0, [sp, #S_PSR]                @ irqs are already disabled

 216        msr     spsr_cxsf, r0

 221        ldmia   sp, {r0 - pc}^                  @ load r0 - pc, cpsr

 222

 223        .ltorg

 

 

當前處於用戶空間時,對於ARM系統來講是處於USR模式,此時產生中斷,中斷處理完畢後,不管是不是可剝奪內核,都調轉到統一的用戶模式出口ret_to_user,其檢查是否須要進行進程調度,若須要進行進程調度,則進程切換,不然直接返回到被中斷的用戶空間。

 

 404        .align  5

 405__irq_usr:

 406        usr_entry

 407

 411        get_thread_info tsk

 412#ifdef CONFIG_PREEMPT

 413        ldr     r8, [tsk, #TI_PREEMPT]          @ get preempt count

 414        add     r7, r8, #1                      @ increment it

 415        str     r7, [tsk, #TI_PREEMPT]

 416#endif

 417

 418        irq_handler

 419#ifdef CONFIG_PREEMPT

 420        ldr     r0, [tsk, #TI_PREEMPT]

 421        str     r8, [tsk, #TI_PREEMPT]

 422        teq     r0, r7

 423        strne   r0, [r0, -r0]       @ bug()

 424#endif

 428

 429        mov     why, #0

 430        b       ret_to_user

 432        .ltorg

 

 

1.3.2      保存現場

 105/*

 106 * SVC mode handlers

 107 */

 108

 115        .macro  svc_entry

 116        sub     sp, sp, #S_FRAME_SIZE

 117 SPFIX( tst     sp, #4          )

 118 SPFIX( bicne   sp, sp, #4      )

 119        stmib   sp, {r1 - r12}

 120

 121        ldmia   r0, {r1 - r3}

 122        add     r5, sp, #S_SP           @ here for interlock avoidance

 123        mov     r4, #-1                 @  ""  ""      ""       ""

 124        add     r0, sp, #S_FRAME_SIZE   @  ""  ""      ""       ""

 125 SPFIX( addne   r0, r0, #4      )

 126        str     r1, [sp]                @ save the "real" r0 copied

 127                                        @ from the exception stack

 128

 129        mov     r1, lr

 130

 131        @

 132        @ We are now ready to fill in the remaining blanks on the stack:

 133        @

 134        @  r0 - sp_svc

 135        @  r1 - lr_svc

 136        @  r2 - lr_<exception>, already fixed up for correct return/restart

 137        @  r3 - spsr_<exception>

 138        @  r4 - orig_r0 (see pt_regs definition in ptrace.h)

 139        @

 140        stmia   r5, {r0 - r4}

 141        .endm

 

1.3.3      中斷處理

由於C的調用慣例是要把函數參數放在棧的頂部,所以pt- regs結構包含原始寄存器的值,這些值是之前在彙編入口例程svc_entry中保存在棧中的。

linux+v2.6.19/include/asm-arm/arch-at91rm9200/entry-macro.S

  18        .macro  get_irqnr_and_base, irqnr, irqstat, base, tmp

  19        ldr     /base, =(AT91_VA_BASE_SYS)              @ base virtual address of SYS peripherals

  20        ldr     /irqnr, [/base, #AT91_AIC_IVR]          @ read IRQ vector register: de-asserts nIRQ to processor (and clears interrupt)

  21        ldr     /irqstat, [/base, #AT91_AIC_ISR]        @ read interrupt source number

  22        teq     /irqstat, #0                            @ ISR is 0 when no current interrupt, or spurious interrupt

  23        streq   /tmp, [/base, #AT91_AIC_EOICR]          @ not going to be handled further, then ACK it now.

  24        .endm

 

  26/*

  27 * Interrupt handling.  Preserves r7, r8, r9

  28 */

  29        .macro  irq_handler

  301:      get_irqnr_and_base r0, r6, r5, lr

  31        movne   r1, sp

  32        @

  33        @ routine called with r0 = irq number, r1 = struct pt_regs *

  34        @

  35        adrne   lr, 1b

  36        bne     asm_do_IRQ

  58        .endm

 

 

中斷號的值也在irq_handler初期得以保存,因此,asm_do_IRQ能夠將它提取出來。這個中斷處理程序實際上要調用do_IRQ(),而do_IRQ()要調用handle_IRQ_event()函數,最後這個函數才真正地執行中斷服務例程(ISR)。下圖給出它們的調用關係:

 

 

 
 

 

 
 

 

 
 

 

 
 

 

 
 

 

 

 


 

               

 

 

 

 

 

 

 

 

 

                               中斷處理函數的調用關係

 

1.3.3.1          asm_do_IRQ

 112asmlinkage void asm_do_IRQ(unsigned int irq, struct pt_regs *regs)

 113{

 114        struct pt_regs *old_regs = set_irq_regs(regs);

 115        struct irqdesc *desc = irq_desc + irq;

 116

 121        if (irq >= NR_IRQS)

 122                desc = &bad_irq_desc;

 123

 124        irq_enter(); //記錄硬件中斷狀態,便於跟蹤中斷狀況肯定是不是中斷上下文

 125

 126        desc_handle_irq(irq, desc);

///////////////////desc_handle_irq

  33static inline void desc_handle_irq(unsigned int irq, struct irq_desc *desc)

  34{

  35        desc->handle_irq(irq, desc); //一般handle_irq指向__do_IRQ

  36}

///////////////////desc_handle_irq

 130

 131        irq_exit(); //中斷退出前執行可能的軟中斷,被中斷前是在中斷上下文中則直接退出,這保證了軟中斷不會嵌套

 132        set_irq_regs(old_regs);

 133}

 

1.3.3.2          __do_IRQ

 157 * __do_IRQ - original all in one highlevel IRQ handler

 167fastcall unsigned int __do_IRQ(unsigned int irq)

 168{

 169        struct irq_desc *desc = irq_desc + irq;

 170        struct irqaction *action;

 171        unsigned int status;

 172

 173        kstat_this_cpu.irqs[irq]++;

 186

 187        spin_lock(&desc->lock);

 188        if (desc->chip->ack) //首先響應中斷,一般實現爲關閉本中斷線

 189                desc->chip->ack(irq);

 190       

 194        status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);

 195        status |= IRQ_PENDING; /* we _want_ to handle it */

 196

 201        action = NULL;

 202        if (likely(!(status & (IRQ_DISABLED | IRQ_INPROGRESS)))) {

 203                action = desc->action;

 204                status &= ~IRQ_PENDING; /* we commit to handling */

 205                status |= IRQ_INPROGRESS; /* we are handling it */

 206        }

 207        desc->status = status;

 208

 215        if (unlikely(!action))

 216                goto out;

 217

 218        /*

 219         * Edge triggered interrupts need to remember

 220         * pending events.

 227         */

 228        for (;;) {

 229                irqreturn_t action_ret;

 230

 231                spin_unlock(&desc->lock);//解鎖,中斷處理期間能夠響應其餘中斷,不然再次進入__do_IRQ時會死鎖

 233                action_ret = handle_IRQ_event(irq, action);

 237                spin_lock(&desc->lock);

 238                if (likely(!(desc->status & IRQ_PENDING)))

 239                        break;

 240                desc->status &= ~IRQ_PENDING;

 241        }

 242        desc->status &= ~IRQ_INPROGRESS;

 243

 244out:

 249        desc->chip->end(irq);

 250        spin_unlock(&desc->lock);

 251

 252        return 1;

 253}

 

 

該函數的實現用到中斷線的狀態,下面給予具體說明:

#define IRQ_INPROGRESS  1   /* 正在執行這個IRQ的一個處理程序*/

#define IRQ_DISABLED    2    /* 由設備驅動程序已經禁用了這條IRQ中斷線 */

#define IRQ_PENDING     4    /* 一個IRQ已經出如今中斷線上,且被應答,但尚未爲它提供服務 */

#define IRQ_REPLAY      8    /* 當Linux從新發送一個已被刪除的IRQ時 */

#define IRQ_WAITING     32   /*當對硬件設備進行探測時,設置這個狀態以標記正在被測試的irq */

#define IRQ_LEVEL       64    /* IRQ level triggered */

#define IRQ_MASKED      128    /* IRQ masked - shouldn't be seen again */

#define IRQ_PER_CPU     256     /* IRQ is per CPU */

這8個狀態的前5個狀態比較經常使用,所以咱們給出了具體解釋。

 

經驗代表,應該避免在同一條中斷線上的中斷嵌套,內核經過IRQ_PENDING標誌位的應用保證了這一點。當do_IRQ()執行到for (;;)循環時,desc->status 中的IRQ_PENDING的標誌位確定爲0。當CPU執行完handle_IRQ_event()函數返回時,若是這個標誌位仍然爲0,那麼循環就此結束。若是這個標誌位變爲1,那就說明這條中斷線上又有中斷產生(對單CPU而言),因此循環又執行一次。經過這種循環方式,就把可能發生在同一中斷線上的嵌套循環化解爲「串行」。

 

在循環結束後調用desc->handler->end()函數,具體來講,若是沒有設置IRQ_DISABLED標誌位,就啓用這條中斷線。

 

1.3.3.3          handle_IRQ_event

當執行到for (;;)這個無限循環時,就準備對中斷請求隊列進行處理,這是由handle_IRQ_event()函數完成的。由於中斷請求隊列爲一臨界資源,所以在進入這個函數前要加鎖。

handle_IRQ_event執行全部的irqaction鏈表:

 130irqreturn_t handle_IRQ_event(unsigned int irq, struct irqaction *action)

 131{

 132        irqreturn_t ret, retval = IRQ_NONE;

 133        unsigned int status = 0;

 134

 135        handle_dynamic_tick(action);

 136       // 若是沒有設置IRQF_DISABLED,則中斷處理過程當中,打開中斷

 137        if (!(action->flags & IRQF_DISABLED))

 138                local_irq_enable_in_hardirq();

 139

 140        do {

 141                ret = action->handler(irq, action->dev_id);

 142                if (ret == IRQ_HANDLED)

 143                        status |= action->flags;

 144                retval |= ret;

 145                action = action->next;

 146        } while (action);

 147

 150        local_irq_disable();

 151

 152        return retval;

 153}

 

       

這個循環依次調用請求隊列中的每一箇中斷服務例程。這裏要說明的是,若是設置了IRQF_DISABLED,則中斷服務例程在關中斷的條件下進行(不包括非屏蔽中斷),但一般CPU在穿過中斷門時自動關閉中斷。可是,關中斷時間毫不能太長,不然就可能丟失其它重要的中斷。也就是說,中斷服務例程應該處理最緊急的事情,而把剩下的事情交給另一部分來處理。即後半部分(bottom half)來處理,這一部份內容將在下一節進行討論。

 

不一樣的CPU不容許併發地進入同一中斷服務例程,不然,那就要求全部的中斷服務例程必須是「可重入」的純代碼。可重入代碼的設計和實現就複雜多了,所以,Linux在設計內核時巧妙地「避難就易」,以解決問題爲主要目標。

 

1.3.3.4          irq_exit()

中斷退出前執行可能的軟中斷,被中斷前是在中斷上下文中則直接退出,這保證了軟中斷不會嵌套

////////////////////////////////////////////////////////////

linux+v2.6.19/kernel/softirq.c

 285void irq_exit(void)

 286{

 287        account_system_vtime(current);

 288        trace_hardirq_exit();

 289        sub_preempt_count(IRQ_EXIT_OFFSET);

 290        if (!in_interrupt() && local_softirq_pending())

 291                invoke_softirq();

////////////

 276#ifdef __ARCH_IRQ_EXIT_IRQS_DISABLED

 277# defineinvoke_softirq()       __do_softirq()

 278#else

 279# defineinvoke_softirq()       do_softirq()

 280#endif

////////////

 292        preempt_enable_no_resched();

 293}

////////////////////////////////////////////////////////////

 

1.3.4      從中斷返回

asm_do_IRQ()這個函數處理全部外設的中斷請求後就要返回。返回狀況取決於中斷前程序是內核態仍是用戶態以及是不是可剝奪內核。

²      內核態可剝奪內核,只有在preempt_count爲0時,schedule()纔會被調用,其檢查是否須要進行進程切換,須要的話就切換。在schedule()返回以後,或者若是沒有掛起的工做,那麼原來的寄存器被恢復,內核恢復到被中斷的內核代碼。

²      內核態不可剝奪內核,則直接返回至被中斷的內核代碼。

²      中斷前處於用戶態時,不管是不是可剝奪內核,統一跳轉到ret_to_user。

 

雖然咱們這裏討論的是中斷的返回,但實際上中斷、異常及系統調用的返回是放在一塊兒實現的,所以,咱們經常以函數的形式提到下面這三個入口點:

ret_to_user()

終止中斷處理程序

ret_slow_syscall ( ) 或者ret_fast_syscall

終止系統調用,即由0x80引發的異常

ret_from_exception(  )

終止除了0x80的全部異常

 

 565/*

 566 * This is the return code to user mode for abort handlers

 567 */

 568ENTRY(ret_from_exception)

 569        get_thread_info tsk

 570        mov     why, #0

 571        b       ret_to_user

 

  57ENTRY(ret_to_user)

  58ret_slow_syscall:

 

由上可知,中斷和異常須要返回用戶空間時以及系統調用完畢後都須要通過統一的出口ret_slow_syscall,以此決定是否進行進程調度切換等。

 

linux+v2.6.19/arch/arm/kernel/entry-common.S

  16        .align  5

  17/*

  18 * This is the fast syscall return path.  We do as little as

  19 * possible here, and this includes saving r0 back into the SVC

  20 * stack.

  21 */

  22ret_fast_syscall:

  23        disable_irq                             @ disable interrupts

  24        ldr     r1, [tsk, #TI_FLAGS]

  25        tst     r1, #_TIF_WORK_MASK

  26        bne     fast_work_pending

  27

  28        @ fast_restore_user_regs

  29        ldr     r1, [sp, #S_OFF + S_PSR]        @ get calling cpsr

  30        ldr     lr, [sp, #S_OFF + S_PC]!        @ get pc

  31        msr     spsr_cxsf, r1                   @ save in spsr_svc

  32        ldmdb   sp, {r1 - lr}^                  @ get calling r1 - lr

  33        mov     r0, r0

  34        add     sp, sp, #S_FRAME_SIZE - S_PC

  35        movs    pc, lr                @ return & move spsr_svc into cpsr

  36

  37/*

  38 * Ok, we need to do extra processing, enter the slow path.

  39 */

  40fast_work_pending:

  41        str     r0, [sp, #S_R0+S_OFF]!          @ returned r0

  42work_pending:

  43        tst     r1, #_TIF_NEED_RESCHED

  44        bne     work_resched

  45        tst     r1, #_TIF_NOTIFY_RESUME | _TIF_SIGPENDING

  46        beq     no_work_pending

  47        mov     r0, sp                          @ 'regs'

  48        mov     r2, why                         @ 'syscall'

  49        bl      do_notify_resume

  50        b       ret_slow_syscall                @ Check work again

  51

  52work_resched:

  53        bl      schedule

  54/*

  55 * "slow" syscall return path.  "why" tells us if this was a real syscall.

  56 */

  57ENTRY(ret_to_user)

  58ret_slow_syscall:

  59        disable_irq                             @ disable interrupts

  60        ldr     r1, [tsk, #TI_FLAGS]

  61        tst     r1, #_TIF_WORK_MASK

  62        bne     work_pending

  63no_work_pending:

  64        @ slow_restore_user_regs

  65        ldr     r1, [sp, #S_PSR]                @ get calling cpsr

  66        ldr     lr, [sp, #S_PC]!                @ get pc

  67        msr     spsr_cxsf, r1                   @ save in spsr_svc

  68        ldmdb   sp, {r0 - lr}^                  @ get calling r1 - lr

  69        mov     r0, r0

  70        add     sp, sp, #S_FRAME_SIZE - S_PC

  71        movs    pc, lr                @ return & move spsr_svc into cpsr

 

進入ret_slow_syscall後,首先關中斷,也就是說,執行這段代碼時CPU不接受任何中斷請求。而後,看調度標誌是否爲非0(tst     r1, #_TIF_NEED_RESCHED),若是調度標誌爲非0,說明須要進行調度,則去調用schedule()函數進行進程調度。

 

 

 

 

【嵌入式Linux學習七步曲之第五篇 Linux內核及驅動編程】中斷服務下半部之工做隊列詳解

【摘要】本文詳解了中斷服務下半部之工做隊列實現機制。介紹了工做隊列的特色、其與tasklet和softirq的區別以及其使用場合。接着分析了工做隊列的三種數據結構的組織形式,在此基礎之上分析了工做隊列執行流程。最後介紹了工做隊列相關的API,如何編寫本身的工做隊列處理程序及定義一個work對象並向內核提交等待調度運行。

 

【關鍵字】中斷下半部,工做隊列,workqueue_structwork_structDECLARE_WORK,schedule_work,schedule_delayed_work ,flush_workqueue,create_workqueue,destroy_workqueue

 

1       工做隊列概述

工做隊列(work queue)是另一種將工做推後執行的形式,它和咱們前面討論的全部其餘形式都不相同。工做隊列能夠把工做推後,交由一個內核線程去執行—這個下半部分老是會在進程上下文執行,但因爲是內核線程,其不能訪問用戶空間。最重要特色的就是工做隊列容許從新調度甚至是睡眠。

 

一般,在工做隊列和軟中斷/tasklet中做出選擇很是容易。可以使用如下規則:

²      若是推後執行的任務須要睡眠,那麼只能選擇工做隊列;

²      若是推後執行的任務須要延時指定的時間再觸發,那麼使用工做隊列,由於其能夠利用timer延時;

²      若是推後執行的任務須要在一個tick以內處理,則使用軟中斷或tasklet,由於其能夠搶佔普通進程和內核線程;

²      若是推後執行的任務對延遲的時間沒有任何要求,則使用工做隊列,此時一般爲可有可無的任務。

 

另外若是你須要用一個能夠從新調度的實體來執行你的下半部處理,你應該使用工做隊列。它是唯一能在進程上下文運行的下半部實現的機制,也只有它才能夠睡眠。這意味着在你須要得到大量的內存時、在你須要獲取信號量時,在你須要執行阻塞式的I/O操做時,它都會很是有用。

 

實際上,工做隊列的本質就是將工做交給內核線程處理,所以其能夠用內核線程替換。可是內核線程的建立和銷燬對編程者的要求較高,而工做隊列實現了內核線程的封裝,不易出錯,因此咱們也推薦使用工做隊列。

2       工做隊列的實現

2.1    工做者線程

工做隊列子系統是一個用於建立內核線程的接口,經過它建立的進程負責執行由內核其餘部分排到隊列裏的任務。它建立的這些內核線程被稱做工做者線程(worker thread)。工做隊列可讓你的驅動程序建立一個專門的工做者線程來處理須要推後的工做。不過,工做隊列子系統提供了一個默認的工做者線程來處理這些工做。所以,工做隊列最基本的表現形式就轉變成了一個把須要推後執行的任務交給特定的通用線程這樣一種接口。

 

默認的工做者線程叫作events/n,這裏n是處理器的編號,每一個處理器對應一個線程。好比,單處理器的系統只有events/0這樣一個線程。而雙處理器的系統就會多一個events/1線程。

 

默認的工做者線程會從多個地方獲得被推後的工做。許多內核驅動程序都把它們的下半部交給默認的工做者線程去作。除非一個驅動程序或者子系統必須創建一個屬於它本身的內核線程,不然最好使用默認線程。不過並不存在什麼東西可以阻止代碼建立屬於本身的工做者線程。若是你須要在工做者線程中執行大量的處理操做,這樣作或許會帶來好處。處理器密集型和性能要求嚴格的任務會由於擁有本身的工做者線程而得到好處。

 

2.2    工做隊列的組織結構

2.2.1      工做隊列workqueue_struct

外部可見的工做隊列抽象,用戶接口,是由每一個CPU的工做隊列組成的鏈表

  64struct workqueue_struct {

  65        struct cpu_workqueue_struct *cpu_wq;

  66        const char *name;

  67        struct list_head list;  /* Empty if single thread */

  68};

²      cpu_wq:本隊列包含的工做者線程;

²      name:全部本隊列包含的線程的公共名稱部分,建立工做隊列時的惟一用戶標識;

²      list:連接本隊列的各個工做線程。

 

在早期的版本中,cpu_wq是用數組維護的,即對每一個工做隊列,每一個CPU包含一個此線程。改爲鏈表的優點在於,建立工做隊列的時候能夠指定只建立一個內核線程,這樣消耗的資源較少。

 

在該結構體裏面,給每一個線程分配一個cpu_workqueue_struct,於是也就是給每一個處理器分配一個,由於每一個處理器都有一個該類型的工做者線程。

 

2.2.2      工做者線程cpu_workqueue_struct

這個結構是針對每一個CPU的,屬於內核維護的結構,用戶不可見。

  43struct cpu_workqueue_struct {

  44

  45        spinlock_t lock;

  46

  47        long remove_sequence;   /* Least-recently added (next to run) */

  48        long insert_sequence;   /* Next to add */

  49

  50        struct list_head worklist;

  51        wait_queue_head_t more_work;

  52        wait_queue_head_t work_done;

  53

  54        struct workqueue_struct *wq;

  55        struct task_struct *thread;

  56

  57        int run_depth;          /* Detect run_workqueue() recursion depth */

  58} ____cacheline_aligned;

²      lock:操做該數據結構的互斥鎖

²      remove_sequence:下一個要執行的工做序號,用於flush

²      insert_sequence:下一個要插入工做的序號

²      worklist:待處理的工做的鏈表頭

²      more_work:標識有工做待處理的等待隊列,插入新工做後喚醒對應的內核線程

²      work_done:處理完的等待隊列,沒完成一個工做後,喚醒可能等待通知處理完成通知的線程

²      wq:所屬的工做隊列節點

²      thread:關聯的內核線程指針

²      run_depth:run_workqueue()循環深度,多處可能調用此函數

 

全部的工做者線程都是用普通的內核線程實現的,它們都要執行worker thread()函數。在它初始化完之後,這個函數執行一個死循環並開始休眠。當有操做被插入到隊列裏的時候,線程就會被喚醒,以便執行這些操做。當沒有剩餘的操做時,它又會繼續休眠。

 

2.2.3      工做work_struct

工做用work_struct結構體表示:

linux+v2.6.19/include/linux/workqueue.h

  14struct work_struct {

  15        unsigned long pending;

  16        struct list_head entry;

  17        void (*func)(void *);

  18        void *data;

  19        void *wq_data;

  20        struct timer_list timer;

  21};

²      Pending:這個工做是否正在等待處理標誌,加入到工做隊列後置此標誌

²      Entry:該工做在鏈表中的入口點,鏈接全部工做

²      Func:該工做執行的回調函數

²      Data:傳遞給處理函數的參數

²      wq_data:本工做所掛接的cpu_workqueue_struct;若須要使用定時器,則其爲工做隊列傳遞給timer

²      timer:延遲的工做隊列所用到的定時器,無需延遲是初始化爲NULL

 

2.2.4      三者的關係

 

 

 

位於最高一層的是工做隊列。系統容許有多種類型的工做隊列存在。每個工做隊列具有一個workqueue_struct,而SMP機器上每一個CPU都具有一個該類的工做者線程cpu_workqueue_struct,系統經過CPU號和workqueue_struct 的鏈表指針及第一個成員cpu_wq能夠獲得每一個CPU的cpu_workqueue_struct結構。

而每一個工做提交時,將連接在當前CPU的cpu_workqueue_struct結構的worklist鏈表中。一般狀況下由當前所註冊的CPU執行此工做,但在flush_work中可能由其餘CPU來執行。或者CPU熱插拔後也將進行工做的轉移。

 

內核中有些部分能夠根據須要來建立工做隊列。而在默認狀況下內核只有events這一種類型的工做隊列。大部分驅動程序都使用的是現存的默認工做者線程。它們使用起來簡單、方便。但是,在有些要求更嚴格的狀況下,驅動程序須要本身的工做者線程。

 

2.3    工做隊列執行的細節

工做結構體被鏈接成鏈表,對於某個工做隊列,在每一個處理器上都存在這樣一個鏈表。當一個工做者線程被喚醒時,它會執行它的鏈表上的全部工做。工做被執行完畢,它就將相應的work_struct對象從鏈表上移去。當鏈表上再也不有對象的時候,它就會繼續休眠。

 

此爲工做者線程的標準模板,因此工做者線程都使用此函數。對於用戶自定義的內核線程能夠參考此函數。

 

 233static int worker_thread(void *__cwq)

 234{

 235        struct cpu_workqueue_struct *cwq = __cwq;

// 與該工做者線程關聯的cpu_workqueue_struct結構

 236        DECLARE_WAITQUEUE(wait, current);

// 聲明一個等待節點,若無工做,則睡眠

 237        struct k_sigaction sa;

 238        sigset_t blocked;

 239

 240        current->flags |= PF_NOFREEZE;

 241

 242        set_user_nice(current, -5);

// 設定較低的進程優先級, 工做進程不是個很緊急的進程,不和其餘進程搶佔CPU,一般在系統空閒時運行

 244        /* 禁止並清除全部信號 */

 245        sigfillset(&blocked);

 246        sigprocmask(SIG_BLOCK, &blocked, NULL);

 247        flush_signals(current);

 248

 255        /* SIG_IGN makes children autoreap: see do_notify_parent(). */

// 容許SIGCHLD信號,並設置處理函數

 256        sa.sa.sa_handler = SIG_IGN;

 257        sa.sa.sa_flags = 0;

 258        siginitset(&sa.sa.sa_mask, sigmask(SIGCHLD));

 259        do_sigaction(SIGCHLD, &sa, (struct k_sigaction *)0);

 260

 261        set_current_state(TASK_INTERRUPTIBLE);

// 可被信號中斷,適當的時刻可被殺死,若收到中止命令則退出返回,不然進程就一直運行,無工做可執行時,主動休眠

 262        while (!kthread_should_stop()) {

// 爲了便於remove_wait_queue的統一處理,將當前內核線程添加到cpu_workqueue_structmore_work等待隊列中,當有新work結構鏈入隊列中時會激活此等待隊列

 263                add_wait_queue(&cwq->more_work, &wait);

 

// 判斷是否有工做須要做,無則調度讓出CPU等待喚醒

 264                if (list_empty(&cwq->worklist))

 265                        schedule();

 266                else

 267                        __set_current_state(TASK_RUNNING);

 268                remove_wait_queue(&cwq->more_work, &wait);

// 至此,線程確定處於TASK_RUNNING,從等待隊列中移出

 

//須要再次判斷是由於可能從schedule中被喚醒的。若是有工做作,則執行

 270                if (!list_empty(&cwq->worklist))

 271                        run_workqueue(cwq);

 

// 無工做或者所有執行完畢了,循環整個過程,接着通常會休眠

 272                set_current_state(TASK_INTERRUPTIBLE);

 273        }

 274        __set_current_state(TASK_RUNNING);

 275        return 0;

 276}

 

該函數在死循環中完成了如下功能:

²      線程將本身設置爲休眠狀態TASK_INTERRUPTIBLE並把本身加人到等待隊列上。

²      若是工做鏈表是空的,線程調用schedule()函數進入睡眠狀態。

²      若是鏈表中有對象,線程不會睡眠。相反,它將本身設置成TASK_RUNNING,脫離等待隊列。

²      若是鏈表非空,調用run_workqueue函數執行被推後的工做。

 

run_workqueue執行具體的工做,多處會調用此函數。在調用Flush_work時爲防止死鎖,主動調用run_workqueue,此時可能致使多層次遞歸。

 196static void run_workqueue(struct cpu_workqueue_struct *cwq)

 197{

 198        unsigned long flags;

 199

 204        spin_lock_irqsave(&cwq->lock, flags);

// 統計已經遞歸調用了多少次了

 205        cwq->run_depth++;

 206        if (cwq->run_depth > 3) {

 207                /* morton gets to eat his hat */

 208                printk("%s: recursion depth exceeded: %d/n",

 209                        __FUNCTION__, cwq->run_depth);

 210                dump_stack();

 211        }

 212        while (!list_empty(&cwq->worklist)) {

 213                struct work_struct *work = list_entry(cwq->worklist.next,

 214                                                struct work_struct, entry);

 215                void (*f) (void *) = work->func;

 216                void *data = work->data;

 217                        //將當前節點從鏈表中刪除並初始化其entry

 218                list_del_init(cwq->worklist.next);

 219                spin_unlock_irqrestore(&cwq->lock, flags);

 220

 221                BUG_ON(work->wq_data != cwq);

 222                clear_bit(0, &work->pending); //清除pengding位,標示已經執行

 223                f(data);

 224

 225                spin_lock_irqsave(&cwq->lock, flags);

 226                cwq->remove_sequence++;

// // 喚醒可能等待的進程,通知其工做已經執行完畢

 227                wake_up(&cwq->work_done);

 228        }

 229        cwq->run_depth--;

 230        spin_unlock_irqrestore(&cwq->lock, flags);

 231}

 

3       工做隊列的API

3.1    API列表

 

功能描述

對應API函數

附註

靜態定義一個工做

DECLARE_WORK(n, f, d)

 

動態建立一個工做

INIT_WORK(_work, _func, _data)

 

工做原型

void work_handler(void *data)

 

將工做添加到指定的工做隊列中

queue_work(struct workqueue_struct *wq, struct work_struct *work)

 

將工做添加到keventd_wq隊列中

schedule_work(struct work_struct *work)

 

延遲delay個tick後將工做添加到指定的工做隊列中

queue_delayed_work(struct workqueue_struct *wq,

struct work_struct *work, unsigned long delay)

 

延遲delay個tick後將工做添加到keventd_wq隊列中

schedule_delayed_work(struct work_struct *work, unsigned long delay)

 

刷新等待指定隊列中的全部工做完成

flush_workqueue(struct workqueue_struct *wq)

 

刷新等待keventd_wq中的全部工做完成

flush_scheduled_work(void)

 

取消指定隊列中全部延遲工做

cancel_delayed_work(struct work_struct *work)

 

建立一個工做隊列

create_workqueue(name)

 

建立一個單線程的工做隊列

create_singlethread_workqueue(name)

 

銷燬指定的工做隊列

destroy_workqueue(struct workqueue_struct *wq)

 

 

3.2    如何建立工做

首先要作的是實際建立一些須要推後完成的工做。能夠經過DECLARE_WORK在編譯時靜態地建立該結構體:

  27#define __WORK_INITIALIZER(n, f, d) {           /

  28        .entry  = { &(n).entry, &(n).entry },         /

  29        .func = (f),                              /

  30        .data = (d),                              /

  31        .timer = TIMER_INITIALIZER(NULL, 0, 0),   /

  32        }

  33

  34#define DECLARE_WORK(n, f, d)                /

  35        struct work_struct n = __WORK_INITIALIZER(n, f, d)

這樣就會靜態地建立一個名爲name,處理函數爲func,參數爲data的work_struct結構體。

 

一樣,也能夠在運行時經過指針建立一個工做:

  40#define PREPARE_WORK(_work, _func, _data)       /

  41        do {                                    /

  42                (_work)->func = _func;             /

  43                (_work)->data = _data;            /

  44        } while (0)

  45

 

  49#define INIT_WORK(_work, _func, _data)         /

  50        do {                                        /

  51                INIT_LIST_HEAD(&(_work)->entry);      /

  52                (_work)->pending = 0;                      /

  53                PREPARE_WORK((_work), (_func), (_data));   /

  54                init_timer(&(_work)->timer);                 /

  55        } while (0)

這會動態地初始化一個由work指向的工做,處理函數爲func,參數爲data。

不管是動態仍是靜態建立,默認定時器初始化爲0,即不進行延時調度。

 

3.3    工做隊列處理函數

工做隊列處理函數的原型是:

void work_handler(void *data)

這個函數會由一個工做者線程執行,所以,函數會運行在進程上下文中。默認狀況下,容許響應中斷,而且不持有任何鎖。若是須要,函數能夠睡眠。須要注意的是,儘管操做處理函數運行在進程上下文中,但它不能訪問用戶空間,由於內核線程在用戶空間沒有相關的內存映射。一般在系統調用發生時,內核會表明用戶空間的進程運行,此時它才能訪問用戶空間,也只有在此時它纔會映射用戶空間的內存。

 

在工做隊列和內核其餘部分之間使用鎖機制就像在其餘的進程上下文中使用鎖機制同樣方便。這使編寫處理函數變得相對容易。

 

3.4    調度工做

3.4.1      queue_work

建立一個工做的時候無須考慮工做隊列的類型。在建立以後,能夠調用下面列舉的函數。這些函數與schedule-work()以及schedule-delayed-Work()相近,唯一的區別就在於它們針對給定的工做隊列而不是默認的event隊列進行操做。

 

將工做添加到當前處理器對應的鏈表中,但並不能保證此工做由提交該工做的CPU執行。Flushwork時可能執行全部CPU上的工做或者CPU熱插拔後將進行工做的轉移

 107int fastcall queue_work(struct workqueue_struct *wq, struct work_struct *work)

 108{

 109        int ret = 0, cpu = get_cpu();

// 工做結構還沒在隊列, 設置pending標誌表示把工做結構掛接到隊列中

 111        if (!test_and_set_bit(0, &work->pending)) {

 112                if (unlikely(is_single_threaded(wq)))

 113                        cpu = singlethread_cpu;

 114                BUG_ON(!list_empty(&work->entry));

 115                __queue_work(per_cpu_ptr(wq->cpu_wq, cpu), work);

////////////////////////////////

  84static void __queue_work(struct cpu_workqueue_struct *cwq,

  85                         struct work_struct *work)

  86{

  87        unsigned long flags;

  88

  89        spin_lock_irqsave(&cwq->lock, flags);

 //// 指向CPU工做隊列

  90        work->wq_data = cwq;

// 加到隊列尾部

  91        list_add_tail(&work->entry, &cwq->worklist);

  92        cwq->insert_sequence++;

// 喚醒工做隊列的內核處理線程

  93        wake_up(&cwq->more_work);

  94        spin_unlock_irqrestore(&cwq->lock, flags);

  95}

////////////////////////////////////

 116                ret = 1;

 117        }

 118        put_cpu();

 119        return ret;

 120}

 121EXPORT_SYMBOL_GPL(queue_work);

 

一旦其所在的處理器上的工做者線程被喚醒,它就會被執行。

 

3.4.2      schedule_work

在大多數狀況下, 並不須要本身創建工做隊列,而是隻定義工做, 將工做結構掛接到內核預約義的事件工做隊列中調度, 在kernel/workqueue.c中定義了一個靜態全局量的工做隊列static struct workqueue_struct *keventd_wq;

 

調度工做結構, 將工做結構添加到全局的事件工做隊列keventd_wq,調用了queue_work通用模塊。對外屏蔽了keventd_wq的接口,用戶無需知道此參數,至關於使用了默認參數。keventd_wq由內核本身維護,建立,銷燬。

 

 455static struct workqueue_struct *keventd_wq;

 463int fastcall schedule_work(struct work_struct *work)

 464{

 465        return queue_work(keventd_wq, work);

 466}

 467EXPORT_SYMBOL(schedule_work);

 

3.4.3      queue_delayed_work

有時候並不但願工做立刻就被執行,而是但願它通過一段延遲之後再執行。在這種狀況下,

同時也能夠利用timer來進行延時調度,到期後才由默認的定時器回調函數進行工做註冊。

 

延遲delay後,被定時器喚醒,將work添加到工做隊列wq中。

 143int fastcall queue_delayed_work(struct workqueue_struct *wq,

 144                        struct work_struct *work, unsigned long delay)

 145{

 146        int ret = 0;

 147        struct timer_list *timer = &work->timer;

 148

 149        if (!test_and_set_bit(0, &work->pending)) {

 150                BUG_ON(timer_pending(timer));

 151                BUG_ON(!list_empty(&work->entry));

 152

 153                /* This stores wq for the moment, for the timer_fn */

 154                work->wq_data = wq;

 155                timer->expires = jiffies + delay;

 156                timer->data = (unsigned long)work;

 157                timer->function = delayed_work_timer_fn;

////////////////////////////////////

定時器到期後執行的默認函數,其將某個work添加到一個工做隊列中,需兩個重要信息:

Work:__data定時器的惟一參數

待添加至的隊列:由work->wq_data提供

 123static void delayed_work_timer_fn(unsigned long __data)

 124{

 125        struct work_struct *work = (struct work_struct *)__data;

 126        struct workqueue_struct *wq = work->wq_data;

 127        int cpu = smp_processor_id();

 128

 129        if (unlikely(is_single_threaded(wq)))

 130                cpu = singlethread_cpu;

 131

 132        __queue_work(per_cpu_ptr(wq->cpu_wq, cpu), work);

 133}

////////////////////////////////////

 158                add_timer(timer);

 159                ret = 1;

 160        }

 161        return ret;

 162}

 163EXPORT_SYMBOL_GPL(queue_delayed_work);

 

3.4.4      schedule_delayed_work

其利用queue_delayed_work實現了默認線程keventd_wq中工做的調度。

 

 477int fastcall schedule_delayed_work(struct work_struct *work, unsigned long delay)

 478{

 479        return queue_delayed_work(keventd_wq, work, delay);

 480}

 481EXPORT_SYMBOL(schedule_delayed_work);

 

3.5    刷新工做

3.5.1      flush_workqueue

排入隊列的工做會在工做者線程下一次被喚醒的時候執行。有時,在繼續下一步工做以前,你必須保證一些操做已經執行完畢了。這一點對模塊來講就很重要,在卸載以前,它就有可能須要調用下面的函數。而在內核的其餘部分,爲了防止竟爭條件的出現,也可能須要確保再也不有待處理的工做。

 

出於以上目的,內核準備了一個用於刷新指定工做隊列的函數flush_workqueue。其確保全部已經調度的工做已經完成了,不然阻塞直到其執行完畢,一般用於驅動模塊的關閉處理。其檢查已經每一個CPU上執行完的序號是否大於此時已經待插入的序號。對於新的之後插入的工做,其不受影響。

 320void fastcall flush_workqueue(struct workqueue_struct *wq)

 321{

 322        might_sleep();

 323

 324        if (is_single_threaded(wq)) {

 325                /* Always use first cpu&apos;s area. */

 326                flush_cpu_workqueue(per_cpu_ptr(wq->cpu_wq, singlethread_cpu));

 327        } else {

 328                int cpu;

// 被保護的代碼可能休眠,故此處使用內核互斥鎖而非自旋鎖

 330                mutex_lock(&workqueue_mutex);

// 將同時調度其餘CPU上的工做,這說明了工做並不是在其註冊的CPU上執行

 331                for_each_online_cpu(cpu)

 332                        flush_cpu_workqueue(per_cpu_ptr(wq->cpu_wq, cpu));

//////////////////////////

 278static void flush_cpu_workqueue(struct cpu_workqueue_struct *cwq)

 279{

 280        if (cwq->thread == current) {

// keventd自己須要刷新全部工做時,手動調用run_workqueue,不然將形成死鎖。

 285                run_workqueue(cwq);

 286        } else {

 287                DEFINE_WAIT(wait);

 288                long sequence_needed;

 289

 290                spin_lock_irq(&cwq->lock);

// 保存隊列中當前已有的工做所處的位置,不用等待新插入的工做執行完畢

 291                sequence_needed = cwq->insert_sequence;

 292

 293                while (sequence_needed - cwq->remove_sequence > 0) {

// 若是隊列中還有未執行完的工做,則休眠

 294                        prepare_to_wait(&cwq->work_done, &wait,

 295                                        TASK_UNINTERRUPTIBLE);

 296                        spin_unlock_irq(&cwq->lock);

 297                        schedule();

 298                        spin_lock_irq(&cwq->lock);

 299                }

 300                finish_wait(&cwq->work_done, &wait);

 301                spin_unlock_irq(&cwq->lock);

 302        }

 303}

//////////////////////////

 333                mutex_unlock(&workqueue_mutex);

 334        }

 335}

 336EXPORT_SYMBOL_GPL(flush_workqueue);

 

函數會一直等待,直到隊列中全部對象都被執行之後才返回。在等待全部待處理的工做執行的時候,該函數會進入休眠狀態,因此只能在進程上下文中使用它。

 

注意,該函數並不取消任何延遲執行的工做。就是說,任何經過schedule_delayed_work調度的工做,若是其延遲時間未結束,它並不會由於調用flush_scheduled_work()而被刷新掉。

 

3.5.2      flush_scheduled_work

 

刷新系統默認工做線程的函數爲flush_scheduled_work,其調用了上面通用的函數

 532void flush_scheduled_work(void)

 533{

 534        flush_workqueue(keventd_wq);

 535}

 536EXPORT_SYMBOL(flush_scheduled_work);

 

3.5.3      cancel_delayed_work

取消延遲執行的工做應該調用:

int cancel_delayed_work(struct work_struct *work);

這個函數能夠取消任何與work_struct相關的掛起工做。

 

3.6    建立新的工做隊列

若是默認的隊列不能知足你的須要,你應該建立一個新的工做隊列和與之相應的工做者線程。因爲這麼作會在每一個處理器上都建立一個工做者線程,因此只有在你明確了必需要靠本身的一套線程來提升性能的狀況下,再建立本身的工做隊列。

 

建立一個新的任務隊列和與之相關的工做者線程,只需調用一個簡單的函數:create_workqueue。這個函數會建立全部的工做者線程(系統中的每一個處理器都有一個)而且作好全部開始處理工做以前的準備工做。name參數用於該內核線程的命名。對於具體的線程會更加CPU號添加上序號。

 

create_workqueuecreate_singlethread_workqueue都是建立一個工做隊列,可是差異在於create_singlethread_workqueue能夠指定爲此工做隊列只建立一個內核線程,這樣能夠節省資源,無需發揮SMP的並行處理優點。

 

create_singlethread_workqueue對外進行了封裝,至關於使用了默認參數。兩者同時調用了統一的處理函數__create_workqueue,其對外不可見。

 

  59#define create_workqueue(name) __create_workqueue((name), 0)

  60#define create_singlethread_workqueue(name) __create_workqueue((name), 1)

 

 363struct workqueue_struct *__create_workqueue(const char *name,

 364                                            int singlethread)

 365{

 366        int cpu, destroy = 0;

 367        struct workqueue_struct *wq;

 368        struct task_struct *p;

 369

 370        wq = kzalloc(sizeof(*wq), GFP_KERNEL);

 371        if (!wq)

 372                return NULL;

 373

 374        wq->cpu_wq = alloc_percpu(struct cpu_workqueue_struct);

 375        if (!wq->cpu_wq) {

 376                kfree(wq);

 377                return NULL;

 378        }

 379

 380        wq->name = name;

 381        mutex_lock(&workqueue_mutex);

 382        if (singlethread) {

 383                INIT_LIST_HEAD(&wq->list); //終止鏈表

 384                p = create_workqueue_thread(wq, singlethread_cpu);

 385                if (!p)

 386                        destroy = 1;

 387                else

 388                        wake_up_process(p);

 389        } else {

 390                list_add(&wq->list, &workqueues);

 391                for_each_online_cpu(cpu) {

 392                        p = create_workqueue_thread(wq, cpu);

/////////////////////////////////

 338static struct task_struct *create_workqueue_thread(struct workqueue_struct *wq,

 339                                                   int cpu)

 340{

 341        struct cpu_workqueue_struct *cwq = per_cpu_ptr(wq->cpu_wq, cpu);

 342        struct task_struct *p;

 343

 344        spin_lock_init(&cwq->lock);

 345        cwq->wq = wq;

 346        cwq->thread = NULL;

 347        cwq->insert_sequence = 0;

 348        cwq->remove_sequence = 0;

 349        INIT_LIST_HEAD(&cwq->worklist);

 350        init_waitqueue_head(&cwq->more_work);

 351        init_waitqueue_head(&cwq->work_done);

 352

 353        if (is_single_threaded(wq))

 354                p = kthread_create(worker_thread, cwq, "%s", wq->name);

 355        else

 356                p = kthread_create(worker_thread, cwq, "%s/%d", wq->name, cpu);

 357        if (IS_ERR(p))

 358                return NULL;

 359        cwq->thread = p;

 360        return p;

 361}

/////////////////////////////////

 393                        if (p) {

 394                                kthread_bind(p, cpu);

 395                                wake_up_process(p);

 396                        } else

 397                                destroy = 1;

 398                }

 399        }

 400        mutex_unlock(&workqueue_mutex);

 401

 405        if (destroy) {//若是啓動任意一個線程失敗,則銷燬整個工做隊列

 406                destroy_workqueue(wq);

 407                wq = NULL;

 408        }

 409        return wq;

 410}

 411EXPORT_SYMBOL_GPL(__create_workqueue);

 

3.7    銷燬工做隊列

銷燬一個工做隊列,如有未完成的工做,則阻塞等待其完成。而後銷燬對應的內核線程。

 434void destroy_workqueue(struct workqueue_struct *wq)

 435{

 436        int cpu;

 437

 438        flush_workqueue(wq); //等待全部工做完成

 439/// 利用全局的互斥鎖鎖定全部工做隊列的操做

 441        mutex_lock(&workqueue_mutex);

// 清除相關的內核線程

 442        if (is_single_threaded(wq))

 443                cleanup_workqueue_thread(wq, singlethread_cpu);

 444        else {

 445                for_each_online_cpu(cpu)

 446                        cleanup_workqueue_thread(wq, cpu);

/////////////////////////////////

 413static void cleanup_workqueue_thread(struct workqueue_struct *wq, int cpu)

 414{

 415        struct cpu_workqueue_struct *cwq;

 416        unsigned long flags;

 417        struct task_struct *p;

 418

 419        cwq = per_cpu_ptr(wq->cpu_wq, cpu);

 420        spin_lock_irqsave(&cwq->lock, flags);

 421        p = cwq->thread;

 422        cwq->thread = NULL;

 423        spin_unlock_irqrestore(&cwq->lock, flags);

 424        if (p)

 425                kthread_stop(p); //銷燬該線程,此處可能休眠

 426}

/////////////////////////////////

 447                list_del(&wq->list);

 448        }

 449        mutex_unlock(&workqueue_mutex);

 450        free_percpu(wq->cpu_wq);

 451        kfree(wq);

 452}

 453EXPORT_SYMBOL_GPL(destroy_workqueue);

 

 

 

【嵌入式Linux學習七步曲之第五篇 Linux內核及驅動編程】中斷服務下半部之七姑八姨

【摘要】本文分析了中斷服務下半部存在的必要性,接着介紹了上下半部的分配原則,最後分析了各類下半部機制的歷史淵源,簡單介紹了各類機制的特色。

【關鍵字】下半部,bottom halfBHtaskletsoftirq,工做隊列,內核定時器

 

 

 

 

1       下半部,我思故我在

中斷處理程序是內核中頗有用的—實際上也是必不可少的—部分。可是,因爲自己存在一些侷限,因此它只能完成整個中斷處理流程的上半部分。這些侷限包括:

²      中斷處理程序以異步方式執行而且它有可能會打斷其餘重要代碼(甚至包括其餘中斷處理程序)的執行。所以,爲了不被打斷的代碼中止時間過長,中斷處理程序應該執行得越快越好。

²      若是當前有一箇中斷處理程序正在執行,在最好的狀況下與該中斷同級的其餘中斷會被屏蔽,在最壞的狀況下,當前處理器上全部其餘中斷都會被屏蔽。所以,仍應該讓它們執行得越快越好。

²      因爲中斷處理程序每每須要對硬件進行操做,因此它們一般有很高的時限要求。

²      中斷處理程序不在進程上下文中運行,因此它們不能阻塞。這限制了它們所作的事情。

   

如今,爲何中斷處理程序只能做爲整個硬件中斷處理流程一部分的緣由就很明顯了。咱們必須有一個快速、異步、簡單的處理程序負責對硬件作出迅速響應並完成那些時間要求很嚴格的操做。中斷處理程序很適合於實現這些功能,但是,對於那些其餘的、對時間要求相對寬鬆的任務,就應該推後到中斷被激活之後再去運行。

 

 

這樣,整個中斷處理流程就被分爲了兩個部分,或叫兩半。第一個部分是中斷處理程序(上半部),內核經過對它的異步執行完成對硬件中斷的即時響應。下半部(bottom half)負責其餘響應。

 

 

2       上下半部分家產的原則

下半部的任務就是執行與中斷處理密切相關但中斷處理程序自己不執行的工做。在理想的狀況下,最好是中斷處理程序將全部工做都交給下半部分執行,由於咱們但願在中斷處理程序中完成的工做越少越好(也就是越快越好)。咱們指望中斷處理程序可以儘量快地返回。

 

 

可是,中斷處理程序註定要完成一部分工做。例如,中斷處理程序幾乎都須要經過操做硬件對中斷的到達進行確認。有時它還會從硬件拷貝數據。由於這些工做對時間很是敏感,因此只能靠中斷處理程序本身去完成。

 

 

剩下的幾乎全部其餘工做都是下半部執行的目標。例如,若是你在上半部中把數據從硬件拷貝到了內存,那麼固然應該在下半部中處理它們。遺憾的是,並不存在嚴格明確的規定來講明到底什麼任務應該在哪一個部分中完成—如何作決定徹底取決於驅動程序開發者本身的判斷。記住,中斷處理程序會異步執行,而且即便在最好的狀況下它也會鎖定當前的中斷線。所以將中斷處理程序持續執行的時間縮短到最小很是重要。上半部和下半部之間劃分應大體遵循如下規則:

²      若是一個任務對時間很是敏感,將其放在中斷處理程序中執行;

²      若是一個任務和硬件相關,將其放在中斷處理程序中執行;

²      若是一個任務要保證不被其餘中斷(特別是相同的中斷)打斷,將其放在中斷處理程序中執行;

²      其餘全部任務,考慮放置在下半部執行。

 

 

在決定怎樣把你的中斷處理流程中的工做劃分到上半部和下半部中去的時候,問問本身什麼必須放進上半部而什麼能夠放進下半部。一般,中斷處理程序要執行得越快越好。

 

 

理解爲何要讓工做推後執行以及在何時推後執行很是關鍵。咱們但願儘可能減小中斷處理程序中須要完成的工做量,由於在它運行的時候當前的中斷線在全部處理器上都會被屏蔽。更糟糕的是若是一個處理程序是SA_ INTERRUPT類型,它執行的時候會禁止全部本地中斷。而縮短中斷被屏蔽的時間對系統的響應能力和性能都相當重要。解決的方法就是把一些工做放到之後去作。

 

 

但具體放到之後的何時去作呢?在這裏,之後僅僅用來強調不是立刻而已,理解這一點至關重要。下半部並不須要指明一個確切時間,只要把這些任務推遲一點,讓它們在系統不太繁忙而且中斷恢復後執行就能夠了。一般下半部在中斷處理程序一返回就會立刻運行。下半部執行的關鍵在於當它們運行的時候,容許響應全部的中斷。

 

 

不只僅是Linux,許多操做系統也把處理硬件中斷的過程分爲兩個部分。上半部分簡單快速,執行的時候禁止一些或者所有中斷。下半部分稍後執行,並且執行期間能夠響應全部的中斷。這種設計可以使系統處幹中斷屏蔽狀態的時間儘量的短,以此來提升系統的響應能力。

 

 

3       下半部之七姑八姨

和上半部分只能經過中斷處理程序實現不一樣,下半部能夠經過多種機制實現。這些用來實現下半部的機制分別由不一樣的接口和子系統組成。實際上,在Linux發展的過程當中曾經出現過多種下半部機制。讓人倍受困擾的是,其中很多機制名字起得很相像,甚至還有一些機制名字起得辭不達意。

 

 

最先的Linux只提供「bottom half」這種機制用於實現下半部。這個名字在那個時候毫無異義,由於當時它是將工做推後的唯一方法。這種機制也被稱爲「BH",咱們如今也這麼叫它,以免和「下半部」這個通用詞彙混淆。

 

 

BH接口也很是簡單。它提供了一個靜態建立、由32bottom half組成的數組。上半部經過一個32位整數中的一位來標識出哪一個bottom half能夠執行。每一個BH都在全局範圍內進行同步。對於本地CPU,嚴格的串行執行,當被中斷重入後,若發現中斷前已經在執行BH則退出。即便分屬於不一樣的處理器,也不容許任何兩個bottom half同時執行。若發現另外一CPU正在執行,則退出。這種機制使用方便卻不夠靈活,簡單卻有性能瓶頸。

 

 

不久,內核開發者們就引入了任務隊列(task queue)機制來實現工做的推後執行,並用它來代替BH機制。內核爲此定義了一組隊列,其中每一個隊列都包含一個由等待調用的函數組成鏈表,這樣就至關於實現了二級鏈表,擴展了BH32個的限制。根據其所處隊列的位置,這些函數會在某個時刻被執行。驅動程序能夠把它們本身的下半部註冊到合適的隊列上去。這種機制表現得還不錯,但仍不夠靈活,無法代替整個BH接口。對於一些性能要求較高的子系統,像網絡部分,它也不能勝任。

 

 

2.3這個開發版本中,內核開發者引入了tasklet和軟中斷softirq。若是無須考慮和過去開發的驅動程序兼容的話,軟中斷和tasklet能夠徹底代替BH接口。

 

 

軟中斷是一組靜態定義的下半部接口,有32個,能夠在全部處理器上同時執行—即便兩個類型相同也能夠。

 

 

tasklet是一種基於軟中斷實現的靈活性強、動態建立的下半部實現機制。兩個不一樣類型的tasklet能夠在不一樣的處理器上同時執行,但類型相同的tasklet不能同時執行。tasklet實際上是一種在性能和易用性之間尋求平衡的產物。對於大部分下半部處理來講,用tasklet就足夠了。像網絡這樣對性能要求很是高的狀況才須要使用軟中斷。但是,使用軟中斷須要特別當心,由於兩個相同的軟中斷在SMP上有可能同時被執行。此外,軟中斷由數組組織,還必須在編譯期間就進行靜態註冊,即與某個軟中斷號關聯。與此相反,tasklet爲某個固定的軟中斷號,通過二級擴展,維護了一個鏈表,所以能夠動態註冊刪除。

 

 

在開發2.5版本的內核時,BH接口最終被棄置了,全部的BH使用者必須轉而使用其餘下半部接口。此外,任務隊列接口也被工做隊列接口取代了。工做隊列是一種簡單但頗有用的方法,它們先對要推後執行的工做排隊,稍後在進程上下文中執行它們。

 

 

另一個能夠用於將工做推後執行的機制是內核定時器。不像其餘下半部機制,內核定時器把操做推遲到某個肯定的時間段以後執行。也就是說,儘管本章討論的其餘機制能夠把操做推後到除了如今之外的任什麼時候間進行,可是當你必須保證在一個肯定的時間段過去之後再運行時,你應該使用內核定時器。可是執行定時器註冊的函數時,仍然須要使用軟中斷機制,即定時器引入了一個固定延時和一個軟中斷的可變延時。

 

 

BH轉換爲軟中斷或者tasklet並非垂手可得的事,由於BH是全局同步的,所以,在其執行期間假定沒有其餘BH在執行。可是,這種轉換最終仍是在內核2.5中實現了。

 

 

 「下半部(bottom half)」是一個操做系統通用詞彙,用於指代中斷處理流程中推後執行的那一部分,之因此這樣命名是由於它表示中斷處理方案一半的第二部分或者下半部。全部用於實現將工做推後執行的內核機制都被稱爲「下半部機制」。

 

 

綜上所述,在2.6這個當前版本中,內核提供了三種不一樣形式的下半部實現機制:軟中斷、tasklet和工做隊列。tasklet經過軟中斷實現,而工做隊列與它們徹底不一樣。下半部機制的演化歷程以下:

 

 

 

 

【嵌入式Linux學習七步曲之第五篇 Linux內核及驅動編程】中斷服務下半部之老大-軟中斷softirq

【摘要】本文詳解了中斷服務下半部機制的基礎softirq。首先介紹了其數據結構,分析了softirq的執行時機及流程。接着介紹了軟中斷的API及如何添加本身的軟中斷程序,註冊及其觸發。最後了介紹了用於處理過多軟中斷的內核線程ksoftirqd,分析了觸發ksoftirqd的原則及其執行流程。

 

【關鍵字】中斷服務下半部,軟中斷softirqsoftirq_actionopen_softirq()raise_softirqksoftirqd

 

 

1      軟中斷結構softirq_action

2      執行軟中斷

3      軟中斷的API

3.1       分配索引號

3.2       軟中斷處理程序

3.3       註冊軟中斷處理程序

3.4       觸發軟中斷

4      ksoftirqd

4.1       Ksoftirqd的誕生

4.2       啓用Ksoftirqd的準則

4.3       Ksoftirqd的實現

 

1       軟中斷結構softirq_action

軟中斷使用得比較少,但其是tasklet實現的基礎。而tasklet是下半部更經常使用的一種形式。軟中斷是在編譯期間靜態分配的。它不像tasklet那樣能被動態地註冊或去除。軟中斷由softirq_action結構表示,它定義在<linux/interrupt.h>:

 246struct softirq_action

 247{

 248        void    (*action)(struct softirq_action *);

 249        void    *data;

 250};

Action: 待執行的函數;

Data: 傳給函數的參數,任意類型的指針,在action內部轉化

 

kernel/softirq.c中定義了一個包含有32個該結構體的數組。

static struct softirq_actionsoftirq_vec[32] __cacheline_aligned_in_smp;

每一個被註冊的軟中斷都佔據該數組的一項。所以最多可能有32個軟中斷,由於系統靠一個32位字的各個位來標識是否須要執行某個軟中斷。注意,這是註冊的軟中斷數目的最大值無法動態改變。在當前版本的內核中,這個項中只用到6個。。

 

2       執行軟中斷

一個註冊的軟中斷必須在被標記後纔會執行。這被稱做觸發軟中斷(raising the softirq )。一般,中斷處理程序會在返回前標記它的軟中斷,使其在稍後被執行。因而,在合適的時刻,該軟中斷就會運行。在下列地方,待處理的軟中斷會被檢查和執行:

²      從一個硬件中斷代碼處返回時。

²      ksoftirqd內核線程中。

²      在那些顯式檢查和執行待處理的軟中斷的代碼中,如網絡子系統中。

 

無論是用什麼辦法喚起,軟中斷都要在do_softirq()中執行。若是有待處理的軟中斷,do_softirq()會循環遍歷每個,調用它們的處理程序。

 252#ifndef __ARCH_HAS_DO_SOFTIRQ

 253

 254asmlinkage void do_softirq(void)

 255{

 256        __u32 pending;

 257        unsigned long flags;

 258

 259        if (in_interrupt()) //中斷函數中不能執行軟中斷

 260                return;

 261

 262        local_irq_save(flags);

 263

 264        pending = local_softirq_pending();

 265

 266        if (pending) //只有有軟中斷須要處理時才進入__do_softirq

 267                __do_softirq();

 /////////////////////////

 195/*

 196 * 最多循環執行MAX_SOFTIRQ_RESTART 次若中斷,若仍然有未處理完的,則交由softirqd 在適當的時機處理。須要協調的是延遲和公平性。儘快處理完軟中斷,但不能過渡影響用戶進程的運行。

 203 */

 204#define MAX_SOFTIRQ_RESTART 10

 206asmlinkage void __do_softirq(void)

 207{

 208        struct softirq_action *h;

 209        __u32 pending;

 210        int max_restart = MAX_SOFTIRQ_RESTART;

 211        int cpu;

 212

 213        pending = local_softirq_pending();

 214        account_system_vtime(current);

 215

 216        __local_bh_disable((unsigned long)__builtin_return_address(0));

 217        trace_softirq_enter();

 218

 219        cpu = smp_processor_id();

 220restart:

 221        /* Reset the pending bitmask before enabling irqs */

 222        set_softirq_pending(0);

 223

 224        local_irq_enable();

 225

 226        h = softirq_vec;

 227

 228        do {

 229                if (pending & 1) {

 230                        h->action(h);

 231                        rcu_bh_qsctr_inc(cpu);

 232                }

 233                h++;

 234                pending >>= 1;

 235        } while (pending);

 236

 237        local_irq_disable();

 238

 239        pending = local_softirq_pending();

 240        if (pending && --max_restart)

 241

相關文章
相關標籤/搜索