最近做的項目跟Linux內核的關系比較大,我們的項目需要在用戶態觸發一些 內核態的代碼運行。眾所周知,內核態的代碼是不能直接被用戶態代碼調用的, 用戶態代碼觸發內核態代碼的必須要經過系統調用。
為什麼選擇 ioctl
那麼該如何實現我們的需求呢?有幾種方法:
改寫內核,擴大系統調用表,添加新的系統調用
利用內核模塊,覆蓋沒被使用或這使用頻率很低的一個系統調用的處理函數
利用已有的系統調用,比如ioctl,來“實現”自定義的系統調用。
第一種方法需要修改內核,適用面比較窄;第二種方法hack意味很濃,沒有 被使用的系統調用號有限,不同模塊可能都使用這種機制,可能會產生沖突。最 終我們選擇了第三種方法。下面將一一道來。
ioctl系統調用是用戶態控 制設備的接口,其用戶態原型為
int ioctl(int d, int request, ...)
第一個參數是打開的設備文件的文件描述符,通常是 open系統調用的返回值;第二個參數request是可以自定義的請求號;第三個參 數可以是一個指針,指向一段用戶態內存,用來傳遞參數,也可以是一個整形數 據。函數原型中的'...'並非表示ioctl是可變參數函數,只是為了告訴 編譯器不要檢查第三個參數。
在較新內核中,ioctl的內核態原型為 unlock_ioctl
long unlocked_ioctl(struct file *file, unsigned int request, unsigned long arg);
這個原型可以 在struct file_operation的定義中找到,還有一個compat_ioctl,用於內核為 64位,用戶空間為32位的情形,跟我們的需求關系不大。
傳入的request 和arg就來自於ioctl系統調用的第二個和第三個參數。在內核態中,可以根據 request的值,來調用約定的函數,“實現”自定義的系統調用。
需要注 意的是,request的值並不是可以隨隨便便自定義的,需要遵循一些規則,可以 參考《選擇ioctl命令》(注意它用的ioctl的原型是老內核的)。
ioctl 是用來操作設備的,因此我們需要一個虛擬的設備,以便ioctl能夠工作。
如何實現
實現虛擬設備需要通過內核模塊來實現。這篇文章寫了 如何寫一個入門的內核模塊。
在內核模塊初始化代碼中
用alloc_chrdev_region申請一個設備號
初始化一個struct file_operations類型的全局變量,將open、close、 unlocked_ioctl等成員賦值為我們實現的函數。
利用cdev_add將設備號與file_operations關聯起來
用class_create創建一個設備類
用device_create創建一個虛擬設備
在內核模塊銷毀代碼中
用cdev_del解除設備號與設備操作之間的關聯
用device_destroy銷毀設備
用class_destroy銷毀設備類
用unregister_chrdev_region釋放設備號。
在自定義的unlocked_ioctl中
通過switch-case,根據request號進入某個case
如果目標函數沒有參數,那麼直接調用即可
如果要傳遞給目標函數的參數直接存儲在arg中,則直接讀取arg再調用即 可。
如果要傳遞給目標函數的參數是arg所指向的一段用戶態內存,則需要從用 戶態拷貝到內核態。較少的數據可以用get_user和put_user來讀寫,較多的數據 可以用copy_from_user和copy_to_user來讀寫。准備好參數之後,調用目標函數 。
編寫字符設備驅動可以參考《Linux Device Driver》,網絡上也有大量的 教程,在此不再贅述。