歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux綜合 >> Linux資訊 >> 更多Linux

詳解Linux操作系統設備驅動兼容性

  最新進展

  Linux一直在迅速地發展著,開發人員總是迫切希望改善核心內部,它們並不考慮向後兼容性。這種自由開發導致了不同版本核心提供的設備驅動程序接口之間一定程度的不兼容。不過,在應用級還保持著兼容,除了個別需要與核心特征進行低級交互的應用(象ps)。

  另一方面,設備驅動程序是直接鏈接到核心映象上的,因此必須與數據結構、全局變量、以及由內系統引出的函數發生的改變保持一致。在開發過程中,隨著新特征的加入,內部被修改;新的實現取代了就的實現,因為實踐證明它們更快,更清晰。盡管不兼容性要求程序員在寫模塊時要做一些額外的工作,我認為連續的開發是Linux社區的成功點:嚴格的先後兼容性最終證明是有害的。

  這一章講述2.0.x和2.1.43之間的不同,這些將會與即將推出的2.2發布類似。Linus在前幾個2.1版本中引入了最重要的改變,這樣核心就可以多經歷幾個2.1版本,使得驅動程序的作者有足夠的時間在開發被鎖定以發布穩定的2.2之前來穩定驅動程序。下面的小節介紹驅動程序是如何處理2.0和2.1.43之間的不同的。我已經修改了本書介紹的所有示例代碼,使得它們可以同時在2.0和2.1.43上編譯和運行,以及這之間的大多數版本。驅動程序的新版本可以從O’Reilly的FTP站點上在線例子的v2.1目錄下得到。2.0和2.1之間的兼容性通過頭文件sysdep-2.1.h獲得,它可以與你自己的模塊集成。我選擇不把兼容性擴展到1.2避免了給C代碼加載太多的條件,而且1.2-2.0的不同已經在前面的章節解釋過了。在我將要寫完這本書時,我了解到從2.1.43起又引入了一些小的不兼容性;我不打算對之加以評述,因為我不能保證對這些最新版本的完全支持。

  注意在本章我不會講述2.1開發系列引入的所有新東西。我要做的只是移植2.0模塊,使之可以在2.0和2.1核心上運行。利用2.1的特征意味著放棄對不具有這些特征的2.0發布的支持。2.0版本仍是本書的重點。在寫sysdep-2.1.h時,我已努力使你熟悉新的API,我引入的宏用來使2.1的代碼可以在2.0上跑,而不是相反。

  本章以重要性逐漸降低的順序介紹不兼容性;最重要的不同首先被介紹,次要的細節則在後面介紹。

  模塊化

  在Linux社區中,模塊化變的越來越重要,開發人員決定用一個更清晰的實現取代舊的。頭文件在2.1.18中完全重寫了,一個新的API被引入。如你所期望的,新的實現比舊的要容易使用。為了加載你的模塊,你將需要包modutils-2.1.34甚至更新版本(細節見Documentation/

  Changes)。當與舊的核心一起使用時,這個包可以回到兼容模式,因此你可以用這個新包替換modules-2.0.0,即使你經常在2.0和2.1之間切換。

  引出符號

  符號表的新接口比以前的要容易多了,它依賴於下面的宏:

  EXPORT_NO_SYMBOLS;這個宏與register_symtab(NULL)等價;它可以出現在一個函數的內部或外部,因為它只是指導匯編器,而不產生實際代碼。如果你想在Linux2.0上編譯模塊,這個宏應該在init_module中被使用。

  EXPORT_SYMTAB;如果你打算引出一些符號,那麼模塊必須在包含之前定義這個宏。

  EXPORT_SYMBOL(name);這個宏表明你想引出這個符號名。它必須在任何函數之外使用。

  EXPORT_SYMBOL_NOVERS(name)使用這個宏而不是EXPORT_SYMBOL()強制丟棄版本信息,即使是編譯帶有版本支持的代碼。這對避免一些不必要的重編譯很有用。例如,memset函數將總以同樣的方式工作;引出符號而不帶版本信息允許開發者改變實現(甚至使用的數據類型)而不需insmod標出不兼容性。在模塊化的代碼中不大可能需要這個宏。

  如果這些宏都沒有在你的源碼中使用,那麼所有的非靜態符號都被引出;這與在2.0中一如果這些宏都沒有在你的源碼中使用,那麼所有的非靜態符號都被引出;這與在2.0中一樣。如果這個模塊是從幾個源文件生成的,你可以從任何源文件引出符號,而且還可以在模塊的范圍中共享任何符號。如你所看到的,引出符號表的新方法解決了一些問題,但這個創新也引入了一個重要的不兼容性:一個引出了一些符號的模塊,如果想同時在2.0和2.1上編譯運行,則必須用條件編譯來包含兩個實現。下面是export模塊(v2.1/misc-modules/export.c)如何處理這個問題的當使用2.1.18或更新的核心時,REGISTER_SYMTAB擴展為什麼都不做,因為init_module中沒有什麼需要做的;在函數外使用EXPORT_SYMBOL是引出模塊符號唯一需要做的。

  核心模塊的新的實現利用了ELF二進制格式的特征以獲得更好的靈活性。更特別地,當構核心模塊的新的實現利用了ELF二進制格式的特征以獲得更好的靈活性。更特別地,當構造一個ELF目標文件時,你可以聲明除“正文”、“數據”和“bss”之外的節。一個“節”是一個連續的數據區域,與“段”的概念類似。

  對於2.1,核心模塊必須使用ELF二進制格式編譯。事實上,2.1核心利用了ELF的節(見“處理核心空間錯誤”),只能編譯為ELF。因此模塊的限制並不是個真正限制。使用ELF允許信息域被存在目標文件中。好奇的讀者可以使用objdump –section-headers來觀察節頭,用objdump –section=.modinfo –full-contents來查看模塊特定的信息。實際上,.modinfo一節是用來存儲模塊信息的節,包含被稱做“參數”的值,可以在加載時修改。

  當在2.1上編譯時,一個參數可以用宏如下聲明:MODULE_PARM(variable, type-description);當你在源文件中使用這個宏時,編譯器被告知在目標文件中插入一個描述串;這個描述表明variable是個參數,它的類型對應於type-description。insmod和modprobe查看目標文件,保證你被允許修改variable,同時檢查參數的實際類型。類型檢查對防止不愉快的錯誤非常重要,例如用一個串覆蓋了一個整數,或錯把長整數當成了短整數。按我的觀點,講述宏的最好辦法時給出幾行示例代碼。下面的代碼屬於一個想象的網卡type-description串在頭文件中被非常詳細地介紹,並且為了你的方便,它可以在整個核心源碼中找到。值得給出的一個技巧是如何參數化一個數組的長度,象上面的io。例如,設想網絡驅動程序支持的外圍板子的數目有宏MAX_DEVICES表示,而不是硬寫入的數字4。出於這個目的,頭文件定義了一個宏(__MODULE_STRING),它用C預處理器將一個宏“字符串化”。這個宏可以如下使用:

  

int io[MAX_DEVICES+1]={0,};



MODULE_PARM(io, “1-” __MODULE_STRING(MAX_DEVICES) “i”);

  在前一行中,被“字符串化”的值與其他串接在一起構成目標文件中有意義的串。scull示例模塊也用MODULE_PARM來聲明它的參數(scull_major和其他整數變量)。這在Linux2.0上編譯時可能會出問題,那裡這個宏未定義。我選擇的簡單的修正是在sysdep-2.1.h中定義MODULE_PARM,這樣在與2.0頭文件編譯時,它擴展為空語句。其它有意義的值可以象MODULE_AUTHOR()一樣 存在模塊的.modinfo一節,但它們目前沒有使用。請參考以獲得更多的信息。

  /proc/modules

  /proc/modules的格式在2.1.18中略有改變,而所有的模塊化代碼都被重寫了。盡管這個改變並不影響源碼,你可能對其細節不感興趣,因為/proc/modules在模塊開發時經常被檢查。

  新格式和舊的一樣是面向行的,每行包含下面的域:

  模塊名:這個域與Linux2.0相同。

  模塊大:小這是個十進制數,以字節為單位(而不是內存頁)報告長度。這個模塊的使用計數如果模塊沒有使用計數,這個計數報告-1。這是和新的模塊化代碼一道引入的新特征;你可以寫一個模塊,它的去除可以有一個函數控制而不是使用計數。這個函數判斷模塊是否能夠被卸載。例如,ipv6模塊就使用這個特征。

  可選標志
  標志是文本串,每個都由括號包含,並由空格分隔。參考本模塊的模塊列表。這個列表整體被包含在方括號內,表中的單個名字由空格隔開。

  下面是/proc/modules在2.1.43中的可能內容:

  

morgana% cat /proc/modules
       ipv6                 57164   -1
       netlink                3180    0  [ipv6]
       floppy                45960   1   (autoclean)
       floppy                45960   1   (autoclean)
       monitor                 516   0   (unused)

  在這個屏幕快照中,ipv6沒有使用計數,並依賴於netlink;floppy已經被kerneld加載,由“autoclean”標志給出,monitor是我的一個小工具,控制一些狀態燈,並在系統終止時關掉我的計算機。如你所看到的,它是“unused”,我並不關心它的使用計數。

  文件操作

  有幾個文件操作在2.1裡與2.0有不同的原型。這主要是出於處理大小不能放入32位的文件的需要。其不同由頭文件sysdep-2.1.h處理,它根據使用的核心版本定義了幾個偽類型。文件操作中引入的僅有的顯著創新是poll方法,它用完全不同的實現代替了select方法。

  原型的不同

  四個文件操作表征一個新的原型;它們是:

  

long long (*llseek) (strUCt inode *, struct file *, long long, int);
long (*read) (struct inode *, struct fle *, char *, unsigned long);
long (*write) (struct inode *, struct file *, const char *, unsigned long);
int (*release) (struct inode *, struct file *);
它們在2.0中的對應者是:
       int (*lseek) (struct inode *, struct file *, off_t, int);
       int (*read) (struct inode *, struct file *, char *, int);
       int (*write) (struct inode *, struct file *, const char *, int);
       void (*release) (struct inode *, struct file *);

  如你所見的,其不同在於它們的返回值(它允許了更大的范圍),還有count和offset參數。頭文件sysdep-2.1.h通過定義下面的宏處理這些不同:

  read_write_t這個宏擴展為參數count的類型以及read和write的返回值。

  lssek_t這個宏擴展為llseek的返回值類型。方法名字的改變(從lseek到llseek)並不是個問題,因為你一般在file_operations中並不用名字對域賦值,而是聲明一個靜態結構。

  lseek_off_t lseek的offset參數。

  release_t release方法的返回值;或為void或為int。

  release_return( int return_value)這個宏可以用來從release方法返回。它的參數用來返回一個錯誤代碼:0表示成

  功,負值表示失敗。在比2.1.31老的核心中,這個宏擴展為return,因為這個方法返回void。

  用前面的宏,一個可移植的驅動程序原型是:

  

lseek_t my_lseek(struct inode *, struct file *, lseek_off_t, int);


       read_write_t my_read(struct inode *, struct file *, char *, count_t);
       read_write_t my_write(struct inode *, struct file *, const char *,
count_t);
       release_t my_release(struct inode *, struct file *);

  poll方法
  2.1.23引入了poll系統調用,它是system V中select的對應者(由BSD Unix引入)。不幸的是不可能在select設備方法之上實現poll的功能,所以整個實現用不同的一個代替,它作為select和poll的後端。

  在當前版本的核心中,file_operations中的設備方法也叫poll,與系統調用類似,因為其內部模仿這個系統調用。這個方法的原型是:unsigned int (*poll) (struct file *, poll_table *);

  驅動程序中設備特定的實現主要完成兩個任務:

  l 在一個可能在將來喚醒它的等待隊列中將當前進程排隊。通常,這意味著同時在輸入和輸出隊列中對進程排隊。函數poll_wait被用於這個目的,其工作方式與select_wait非常類似(細節請看第五章“增強的字符設備驅動程序操作”中“select”一節)。

  2 構造一個位掩碼描述設備的狀態,並將其返回給調用者。這些位的值是平台特定的,在中定義,它必須被包含在驅動程序中。在講述位掩碼的每一位前,我想給出一個典型的實現。下面的函數是v2.1/scull/pipe.c的一部分,是/dev/scullpipe的poll方法的實現。scullpipe的內部在第五章介紹過。

  如你所看到的,這個代碼相當簡單。它比對應的select方法要容易。至於select,狀態位計為“可讀”、“可寫”,或“發生例外”(這是select的第三個條件)。poll各位的完全列表在下面給出。“輸入”位列在前面,然後是“輸出”,一個“例外”位列在最後。

  POLLIN如果設備可以被無阻塞地讀,那麼這個位必須被設置。

  POLLRDNORM如果“一般”數據可以被讀,這個位必須被設。一個可讀設備返回(POLLIN

  POLLRDNORM)。POLLRDBAND在目前的核心源碼中這個位不被使用。Unix System V使用這個位報告非0優先級的數據可讀。數據優先級的概念與“Streams”包相關。

  POLLHUP當一個讀設備的進程看到文件結尾時,驅動程序必須設置POLLHUP(掛起)。一個調用select的進程將被告知設備可讀,這由select的功能說明。

  POLLERR設備上發生了一個錯誤條件。當poll被select系統調用調用時,設備被報告為既可讀又可寫,因為read或write將無阻塞地返回一個錯誤代碼。

  POLLOUT如果設備可以被無阻塞地寫,這個位在返回值中被設置。

  POLLWRNORM這個位與POLLOUT有相同的含義,有時甚至的確為同一個數。一個可寫的設備返(POLLOUT POLLWRNORM)。

  POLLWRBAND與POLLRDBAND類似,這個位意味著非0優先級的數據可以被寫到設備。只有poll的“數據

  報”實現用到著位,因為一個數據報可以傳送“無團隊數據(out-of-band data)”。select報告設備是可寫的。

  POLLPRI高優先級的數據(“無團隊的”)可以被無阻塞地讀取。這個位導致select報告文件上發生了一個例外條件,因為select將無團隊包作為一個例外條件報告。

  poll的主要問題是它與2.0核心所使用的select方法沒有任何關系。因此,處理這個不同的最好方法是使用條件編譯來編譯合適的函數,而且同時將它們都包含在源文件中。如果當前版本支持select而不是poll,那麼頭文件sysdep-2.1.h定義符號__USE_OLD_SELECT__。這將你從在源碼中必須引用LINUX_VERSION_CODE中解脫出來。這兩個函數用同樣的名字調用,因為在結構sample_fops中sample_poll被引用,那裡poll文件操作代替了select方法。

  訪問用戶空間
  核心的第一個2.1版引入了一種從核心代碼訪問用戶空間的新(更好)方法。這個改變修正了一個長期存在的錯誤行為並增強了系統的性能。當你位核心2.1編譯代碼,並需要訪問用戶空間時,你需要包含,而不是。你還必須使用一個與2.0不同的函數集。不用說,頭文件sysdep-2.1.h盡可能地照顧了這些不同,允許你在2.0上編譯時使用2.1的語義。在用戶訪問中最令人注意的不同時verify_area沒有了,因為多數驗證都由CPU完成了。關於這個主題的細節見本章後面的“處理核心空間錯誤”。

  可被用來訪問用戶空間的新的函數集是:

  

int Access_ok(int type, unsigned long addr, unsigned long size);

  如果當前進程被允許訪問地址addr處的內存,函數返回真(1),否則為假(0)。這個函數取代verify_area,盡管它進行較少的檢查。和老的verify_area接收一樣的參數,但是要快的多。在你復引用一個用戶空間地址之前,這個函數應該被調用對之進行檢查;如果你沒有檢查,用戶有可能會訪問和修改核心內存。本章後面的“虛擬內存”一節更細致地解釋了這個問題。幸運的是,下面描述的大多數函數都替你進行了這個檢查,因此你實際上並不需要調用access_ok,除非你選擇這樣做。因此你實際上並不需要調用access_ok,除非你選擇這樣做。int get_user(lvalue, address);在2.1核中使用的宏get_user與我們在2.0中使用的並不相同。其返回值在成功時為0,否則為一個負的錯誤代碼(總是-EFAULT)。這個函數的淨效果是將從地址address取得的數據賦給lvalue。在通常的C語言含義中,這個宏的第一個參數必須是一個lvalue*。與2.0版中的這個函數類似,數據項的實際大小依賴於address參數類型。這個函數在內部調用access_ok。

  

int __get_user(lvalue, address);

  這個函數完全類似get_user,但它不內部調用access_ok。當你訪問一個已經從同一核心函數內部檢查過的用戶地址時,你應該調用__get_user。

  

get_user_ret(lvalue, address, retval);

  這個宏是調用get_user的快捷方式,如果函數失敗則返回retval。

  

int put_user(expression, address);


int __put_user(expression, address);
put_user_ret(expression, address, retval);

  這些函數與它們的get_對應者非常類似,只是它們是向用戶空間寫,而不是讀。成功時,值expression被寫到地址address。

  

unsigned long copy_from_user(unsigned long to, unsigned long from, unsigned
long len);

  這個函數從用戶空間復制數據到核心空間。它代替舊的memcpy_tofs調用。這個函數內部調用access_ok。返回值是未能傳送的字節數。這樣,如果發生錯誤,返回值必然大於0;在那種情況下,驅動程序返回-EFAULT,因為錯誤是由錯誤的內存訪問引起的。

  

unsigned long __copy_from_user(unsigned long to, unsigned long from,
unsigned long len);

  這個函數與copy_from_user一樣,但它不內部調用access_ok。

  

caopy_from_user_ret(to, from, len, retval);

  這個宏是內部調用copy_from_user的快捷方式;如果失敗,則從當前函數返回。

  

unsigned long copy_to_user(unsigned long to, unsigned long from, unsigned
unsigned long copy_to_user(unsigned long to, unsigned long from, unsigned
long len);
unsigned long __copy_to_user(unsigned long to, unsigned long from, unsigned
long len);
copy_to_user(to, from, len, retval);

  這些函數被用來將數據復制到用戶空間,它們的行為非常類似於它們的copy_from的對應者。2.1版核心還定義了其它訪問用戶空間的函數:clear_user,strncpy_from_user,和strlen_user。我不打算討論它們了,因為Linux2.0中沒有這些函數,並且驅動程序的代碼也很少用到它們。有興趣的讀者可以看看。

  使用新的接口
  訪問用戶空間的新的函數集初看起來可能有點令人失望,但它們的確使程序員的日子好過的多了。在Linux2.1上,不再需要顯式地檢查用戶空間;access_ok一般不需要調用。使用新接口的代碼可以直接進行數據傳送。_ret函數在實現系統調用時證明是相當有用的,因為一個用戶空間的失敗通常導致系統調用的一個返回-EFAULT的失敗。因此,一個典型的read實現,看起來如下:

  

long new_read(struct inode *inode, struct file *filp, char *buf,
unsigned long count);
       {
       /* identify your data (device-specific code) */
       if (__copy_to_user(buf, new_data, count))
              return -EFAULT;
       return count;
}

  注意使用不進行檢查的__copy_to_user是因為調用者在把數據傳輸分派到文件操作之前已經檢查了用戶空間。這就象2.0,read和write不需要調用verify_area。類似地,典型的ioctl實現看起來如下:

  

int new_ioctl(struct inode *inode, struct file *filp, unsigned int
cmd, unsigned long arg);
       {
/* device-specific checks, if needed */
switch(cmd){
       case NEW_GETVALUE:
              put_user_ret(new_value, (int *)arg, -EFAULT);
              break;
       case NEW_SETVALUE:
              get_user_ret(new_value, (int *)arg, -EFAULT);
       default:

              return –EINVAL;
       }
return 0;
}

  版本2.0的對應者不同的是,這個函數在switch語句之前並不需要檢查參數,因為每個get_user或put_user會進行檢查。另一方面,當你想寫可以同時在2.0和2.1上編譯的代碼時,問題變得稍微復雜一些,因為在老的核心上,你不能用C預處理器偽裝新的行為。你不能簡單地#define一個接收兩個參數的get_user宏,因為實際的get_user實現在2.0中已經是個宏。我在寫既可移植有高效率的代碼的選擇是設置sysdep-2.1.h以提供具有下列函數的源碼。下面只列出了讀取數據的函數;寫數據的函數行為完全一樣。

  

int access_ok(type, address, size);

  當在2.0上編譯時,這個函數以verify_area的名義實現。int verify_area_20(type, address, size);通常,當為Linux2.1寫代碼時,你不需調用access_ok。另一方面,當在Linux2.0上編譯時,必須調用verify_area。這個函數就是要填平這個不同:當為Linux2.1編譯時,它擴展為空;而為2.0編譯時,它擴展為原來的verify_area。這個函數不能被稱做verify_area,因為2.1已經有一個宏叫這個名字了。在2.1中定義的verify_area宏實現了access_ok的老的語義,它的存在是為了簡化源碼從2.0到2.1的轉換。(從理論上說,你可以在你的模塊中留下verify_area,只是將函數名改一下;這種簡單移植技巧的缺點是新版本不能在2.0上編譯。)

  

int GET_USER(var, add);


int __GET_USER(var, add);
GET_USER_RET(var, add, ret);

  當在2.1上編譯時,這些宏擴展為實際的get_user函數,即上面解釋過的那些。當在2.0上編譯時,get_user的2.0實現被用來實現與2.1中同樣的功能。

  

int copy_from_user(to, from, size);
int copy_from_user(to, from, size);
int __copy_from_user(to, from, size);
copy_from_user_ret(to, from, size);

  當在2.0上編譯時,這些擴展為memcpy_fromfs;而在2.1上,則使用本身的函數。_ret一類在2.0上從不會返回,因為復制函數不會失敗。

  我個人比較喜歡這種實現兼容性的方法,但這並不是唯一的方法。在我的示例代碼中,任何用戶空間的訪問(除了用來read或write的緩沖區,它們已經事先檢查過了)之前,verify_area_20必須被調用。另一種方法更加忠實於2.1的語義,即當用2.0時,在每個get_user 和copy_from_user之前自動生成一個verify_area。這個選擇在源碼級要更清晰一些,但在版本2.0上編譯時效率相當低,包括代碼大小和執行時間。可以同時在2.0和2.1上編譯的示例代碼,如scull模塊,可以在目錄v2.1/scull中找到。我不覺得這個代碼足夠有趣,因此不在這裡給出。

  任務隊列

  從2.1.30開始的Linux版本不再定義函數queue_task_irq和queue_task_irq_off,因為在queue_task上的實際加速不值得花精力維護兩個獨立的函數。當新機制被加到核心時,這就變得明顯了。在源碼級,這是2.0和2.1之間唯一的區別;頭文件定義了消失的函數簡化了從2.0移植驅動程序。感興趣的讀者可以查看以獲得更多的細節。

  中斷管理
  在2.1的開發中,有些Linux內部被修改了。新核心提供了對內部鎖的很好的管理;通過使用幾個細粒度的鎖,而不是全局的鎖,競爭條件被避免了,這樣也獲得了更好的性能----特別是SMP配置下。更細的鎖機制的一個結果是intr_count不再存在了。2.1.34拋棄了這個全局變量,而布爾函數in_interrupt可以取而代之(這個函數從2.1.30開始存在)。目前,in_interrupt是在頭文件中聲明的宏,這個頭文件又包含在中。頭文件sysdep-2.1.h用intr_count的名義定義了in_interrupt以獲得對2.0的向後兼容性。

  注意雖然in_interrupt是個整數,intr_count卻是個unsigned long,因此,如果你想打印這個值,並在2.0和2.1間可移植,你必須強制將這個值轉換為一個顯式的類型,並在調用printk時指定一個合適的格式。在2.1.37中中斷管理又引入了一個不同:快和慢中斷處理程序不再存在了。SA_INTERRUPT不被新版本的request_irq使用,但它在處理程序執行以前仍然控制著中斷是否被打開。如果幾個處理程序共享一個中斷線,每個可以是個不同的“類型”。中斷打開與否依賴於第一個被調用的處理程序。當中斷處理程序存在時,下半部總是執行。

  位操作

  2.1.37稍微改變了在中定義的位操作的作用。現在函數set_bit及其相關者返回void,而新的類似test_and_set_bit的函數已被引入。新的函數集有如下原型:

  

void set_bit(int nr, volatile void * addr);
void clear_bit(int nr, volatile void * addr);
void change_bit(int nr, volatile void * addr);

int test_and_set_bit(int nr, volatile void * addr);
int test_and_clear_bit(int nr, volatile void * addr);
int test_and_change_bit(int nr, volatile void * addr);
int test_bit(nr, addr);

  如果你想獲得與2.0的後向兼容性,你可以在你的模塊中包含sysdep-2.1.h,並使用新的原型。

  轉換函數

  版本2.1.10引入了一個新的轉換函數,在中聲明。這些函數可以用來訪問多字節值,只要這個值已知是小印地安字節序或大印地安字節序。因為這些函數為寫驅動程序代碼提供了很好的快捷方式,頭文件sysdep-2.1.h在較早的版本就已經定義了它們。2.1核心源碼提供的本身實現比sysdep-2.1.h提供的可移植的實現要快,因為它可以利用體系相關的功能。

  新函數對應下面的原型,其中le表示小印地安字節序,be表示大印地安字節序。注意編譯器並不強制嚴格的數據類型化,因為大多數函數都是預處理宏;下面給出的類型僅供參考。

  

__u16 cpu_to_le16(__u16 cpu_val);
       __u32 cpu_to_le32(__u32 cpu_val);
       __u16 cpu_to_be16(__u16 cpu_val);
       __u32 cpu_to_be32(__u32 cpu_val);
       __u16 le16_to_cpu(__u16 le_val);
       __u32 le32_to_cpu(__u32 le_val);
       __u16 be16_to_cpu(__u16 be_val);
       __u32 be32_to_cpu(__u32 be_val);

  這些函數在處理二進制數據流時很有用(例如文件系統數據或存在接口板中的信息),這些函數在處理二進制數據流時很有用(例如文件系統數據或存在接口板中的信息),版本2.1.43又增加了兩個新的轉換函數集。這些集允許你用指針獲取一個值,或是對參數指定的一個值進行就地轉換。對應與16位小印地安字節序的函數又如下的原型;類似的函數對其它類型的整數也存在,導致一共16個函數。

  

__u16 cpu_to_le16p(__u16 *addr)


       __u16 le16_to_cpup(__u16 *addr)
       void cpu_to_le16s(__u16 *addr)
       void le16_to_cpus(__u16 *addr)

  “p”函數類似與指針的復引用,但在需要時轉換這個值;“s”函數可以在原地轉換一個值的印地安字節序(例如,cpu_to_le16s(addr) 和addr=cpu_to_le16(*addr)完成的工作是一樣的)。

  這些函數也在sysdep-2.1.h中定義了。為了避免雙重解釋的副作用,這個頭文件用線入函數,而不是預處理宏。

  

vremap<b></b>
vremap

  “把握內存”中“vmalloc和朋友們”一節描述的vremap函數在版本2.1中得到一個新名字。新函數ioremap只是名字變了,它與舊的remap取同樣的參數。響應的釋放函數是iounmap,它代替vfree來釋放被重映射的地址。這個改變是為了明確這個函數的實際作用:將I/O空間重映射到核心空間的一個虛地址。頭文件sysdep-2.1.h強化了這種新規則,當在2.0版本編譯時,它#define了ioremap和iounmap到它們2.0的對應者。

  虛擬內存
  在核心的版本2.1,Linux的Intel移植對虛擬內存有了一個成熟的視圖。早些的版本的內存管理一直使用“分段”的方法,這是從核心生命期的開始時期繼承下來的。這個改變並不影響驅動程序代碼,但不管怎樣,還是值得一說的。新的規則與Linux的其它移植匹配的起來。虛擬地址空間被構造成核心居於非常高的地址(從3GB往上),而用戶地址空間在0-3GB范圍。當一個進程運行在“管態”時,它可以訪問兩個空間。另一方面,當它運行在“用戶態”時,它不能訪問核心空間,因為屬於核心的頁被標記為“管理員”頁,處理器阻止了對它們的訪問。

  這種內存布局有助於取消舊的memcpy_to_fs一類的函數,因為已經沒有FS段了。核心空間和用戶空間使用同一個“段”,其區別在於CPU所在的優先級。

  處理核心空間錯誤

  Linux核心的2.1版本對從核心空間處理段錯誤的能力有一個極大的增強。本章裡,我准備對其原則給一個快速的概述。新機制對源碼的影響在“訪問用戶空間”中已經描述過。

  如前面所提到過的,核心的最近版本充分利用了ELF二進制格式,特別是考慮到它的在編譯的文件中定義用戶定義的節的能力。編譯器和鏈接器保證屬於同一節的代碼段在可執行文件中一定是連續的,因此當文件被裝載時,在內存中也是連續的。例外處理是通過在核心可執行映象(vmlinux)中定義兩個新節實現的。每次當源碼通過copy_to_user, put_user, 或其讀取的對應者訪問用戶空間時,一些代碼被加到這兩個節中。盡管這看起來是不可忽略的開銷,這個新機制的一個結果是不再需要使用一個昂貴的verify_area。而且,如果使用的用戶地址是正確的,計算流將不會有一個跳轉。當被訪問的用戶地址是無效的時,硬件發出一個頁面錯。錯誤處理程序(在體系結構特定的源碼樹中的do_page_fault)確認這個錯誤是一個“不正確的地址”錯(與“頁不存在”相對),並使用下面的ELF節進行適當的動作:__ex_table這節是個指針對的表。每對的第一個指針指向一個可能因錯誤的用戶空間地址而失敗的指令,第二個值指向一個地址,處理器將在那裡找到幾條的指令來處理這個錯誤。..fixup這節包含指令,處理在__ex_table中描述的所有可能的錯誤。這個表中每對的第二個指針指向居於.fixup中的代碼。

  頭文件負責構造所需的ELF節。訪問用戶空間的每個函數(如put_user)擴展為匯編指令,它將指針加到__ex_table並處理.fixup中的錯。當代碼運行時,實際的執行路徑有一下步驟組成:用於函數“返回值”的處理器寄存器被初始化為0(也就是沒有錯誤),數據被傳送,返回知被傳回調用者。一般的操作的確非常快。如果一個異常發生,do_page_fault打印一條消息,查看__ex_table,跳轉到.fixup,這裡設置返回值為-EFAULT,然後跳轉到訪問用戶空間的指令後位置。

  新的行為可以用faulty(在v2.1/misc-modules目錄)模塊來檢驗。faulty在第四章“調試技巧”中“調試系統錯誤”一節描述。faulty的設備結點通過讀取一個短緩沖區界外來傳送數據到用戶空間,這樣當讀取一個在模塊頁以上的地址時,會導致一個頁面錯。有趣的是注意到這個錯誤依賴於使用核心空間中的一個不正確地址,而大多數情況下異常是有出錯的用戶空間地址造成的。當在PC上用cat命令讀faulty時,下面的消息被打印在控制台上:

  

read: inode c1188348, file c16decf0, buf 0804cbd0, count 4096
       cat: Exception at [<c2807b7>](c2807115)

  前一行是由faulty的read方法打印的,而後者是由錯誤處理程序打印的。第一個數字是錯誤指令的地址,而第二個是修正代碼(在.fixup節中)的地址。

  其它改變

  在2.0和2.1.43之間還有其它一些不同。以我的觀點,它們不需要給予特別的關注,因此我將迅速地概述一下。proc_register_dynamic在2.1.29中消失了。最近的核心對每個/proc文件使用proc_register接口;如果結構proc_dir_entry的low_ino域是0,那麼會被分配一個動態的inode號。當為2.1.29或更新的核心編譯時,頭文件sysdep-2.1.h象proc_register一樣定義proc_register_dynamic;這個在注冊的proc_dir_entry結構以0為inode號時是可行的。在網絡接口驅動程序領域,rebuild_header設備方法從2.1.15起有一個新的原型。如果你只開發以太網驅動程序,你不會關心這個不同,因為以太網驅動程序不實現它們自己的方法;它們依賴於通用的以太網實現。當舊的實現需要時,頭文件sysdep-2.1定義了宏__USE_OLD_REBUILD_HEADER__。示例模塊snull顯示了如何使用這個宏,但每必要在這

  裡給出。

  網絡代碼的另一個改變影響了結構enet_statistics,它從2.1.25起不再存在。代替它的是一個新結構net_device_stats,它在中定義,而不是。新結構與舊結構類似,但是多了兩個域存儲字節計數器:unsigned longrx_bytes, tx_bytes;一個全特征的網絡接口驅動程序應該與rx_packets和tx_packets一道增加這些計數器,盡管一個快速的計劃可能要拋棄這些計數器。核心頭文件將enet_statistics(老結構的名字)定義為net_device_stats(新結構的名字)以方便已有驅動程序的可移植性。



  最後,我需要指出current不再是個全局變量x86, Alpha,以及Sparc的核心移植使用了聰明的技巧將current存在處理器中。這樣核心的開發者努力又擠出了幾個CPU周期。這個技巧避免了大量的內存訪問,有時還能釋放一個通用目的寄存器;編譯器經常分配處理器寄存器來高速緩存幾個經常訪問的內存位置,而current是經常訪問的。在不同的移植中使用了不同的技巧以優化訪問。Alpha和Sparc版本使用一個處理器寄存器(編譯器優化不使用的一個)來存儲current。而Intel處理器有有限數目的寄存器,編譯器可以使用它們所有;在這種情況下技巧包括將結構task_struct和核心棧頁存儲在連續的虛存頁內。這允許current指針被“編碼”在棧指針中。對每個Linux支持的平台,頭文件current.h>給出了實際選擇的實現。象所有重要的軟件一樣,Linux一直在改變著。如果你想為這個最新的、最偉大的核心寫驅動程序,你需要保持跟上核心的發展。盡管處理不兼容性看起來可能很困難,我們發現兩點特性:首先,主要的程序設計技巧一直在那裡,不太可能改變(至少不常);第二,每次改變都變得更好了,經常使你在將來的開發中需要的工作越來越少。



  裡給出。

  網絡代碼的另一個改變影響了結構enet_statistics,它從2.1.25起不再存在。代替它的是一個新結構net_device_stats,它在中定義,而不是。新結構與舊結構類似,但是多了兩個域存儲字節計數器:unsigned longrx_bytes, tx_bytes;一個全特征的網絡接口驅動程序應該與rx_packets和tx_packets一道增加這些計數器,盡管一個快速的計劃可能要拋棄這些計數器。核心頭文件將enet_statistics(老結構的名字)定義為net_device_stats(新結構的名字)以方便已有驅動程序的可移植性。

  最後,我需要指出current不再是個全局變量x86, Alpha,以及Sparc的核心移植使用了聰明的技巧將current存在處理器中。這樣核心的開發者努力又擠出了幾個CPU周期。這個技巧避免了大量的內存訪問,有時還能釋放一個通用目的寄存器;編譯器經常分配處理器寄存器來高速緩存幾個經常訪問的內存位置,而current是經常訪問的。在不同的移植中使用了不同的技巧以優化訪問。Alpha和Sparc版本使用一個處理器寄存器(編譯器優化不使用的一個)來存儲current。而Intel處理器有有限數目的寄存器,編譯器可以使用它們所有;在這種情況下技巧包括將結構task_struct和核心棧頁存儲在連續的虛存頁內。這允許current指針被“編碼”在棧指針中。對每個Linux支持的平台,頭文件current.h>給出了實際選擇的實現。象所有重要的軟件一樣,Linux一直在改變著。如果你想為這個最新的、最偉大的核心寫驅動程序,你需要保持跟上核心的發展。盡管處理不兼容性看起來可能很困難,我們發現兩點特性:首先,主要的程序設計技巧一直在那裡,不太可能改變(至少不常);第二,每次改變都變得更好了,經常使你在將來的開發中需要的工作越來越少。



  裡給出。

  網絡代碼的另一個改變影響了結構enet_statistics,它從2.1.25起不再存在。代替它的是一個新結構net_device_stats,它在中定義,而不是。新結構與舊結構類似,但是多了兩個域存儲字節計數器:unsigned longrx_bytes, tx_bytes;一個全特征的網絡接口驅動程序應該與rx_packets和tx_packets一道增加這些計數器,盡管一個快速的計劃可能要拋棄這些計數器。核心頭文件將enet_statistics(老結構的名字)定義為net_device_stats(新結構的名字)以方便已有驅動程序的可移植性。

  最後,我需要指出current不再是個全局變量x86, Alpha,以及Sparc的核心移植使用了聰明的技巧將current存在處理器中。這樣核心的開發者努力又擠出了幾個CPU周期。這個技巧避免了大量的內存訪問,有時還能釋放一個通用目的寄存器;編譯器經常分配處理器寄存器來高速緩存幾個經常訪問的內存位置,而current是經常訪問的。在不同的移植中使用了不同的技巧以優化訪問。Alpha和Sparc版本使用一個處理器寄存器(編譯器優化不使用的一個)來存儲current。而Intel處理器有有限數目的寄存器,編譯器可以使用它們所有;在這種情況下技巧包括將結構task_struct和核心棧頁存儲在連續的虛存頁內。這允許current指針被“編碼”在棧指針中。對每個Linux支持的平台,頭文件current.h>給出了實際選擇的實現。象所有重要的軟件一樣,Linux一直在改變著。如果你想為這個最新的、最偉大的核心寫驅動程序,你需要保持跟上核心的發展。盡管處理不兼容性看起來可能很困難,我們發現兩點特性:首先,主要的程序設計技巧一直在那裡,不太可能改變(至少不常);第二,每次改變都變得更好了,經常使你在將來的開發中需要的工作越來越少。



Copyright © Linux教程網 All Rights Reserved