[Linux Kernel慢慢學]Different betweeen ioctl, unlocked_ioctl and compat_ioctl

Posted by John on 2020-10-24
Words 1.4k and Reading Time 5 Minutes
Viewed Times

前言

會寫這一篇主要是自己在看一些網路教材的時候如果有些資源比較久你就會發現他用的還是ioctl()接口,但如果你實際上去碰新版的Kernel時卻發現了這個接口卻不見了,但卻多出了unlocked_ioctl()compat_ioctl()這兩個很類似的function,然後就好奇他們到底差在哪裡、要怎麼用,學習完後就打了這篇文章做個記錄。

這篇文章就是對ioctl()unlocked_ioctl()compat_ioctl()三個做一個簡單的介紹,讓大家更能夠知道應該如何使用和區分他們。

(以下linux source code如果沒有特別提及版本,接以v4.1.14為主)

ioctl是什麼?

ioctl()是撰寫driver一個很重要的接口,以字元裝置驅動(char device driver)來說,透過這個接口可以讓user來操作driver執行一些行為。

在撰寫driver code時,我們必須透過register_chrdev()來向kernel註冊我們的driver。為此我們需要提供該driver的file_operation相關函數實作來讓user可以透過這些接口來操控該driver。

在/include/linux/fs.h中可以看到file_operations的結構定義:

1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
/* include/linux/fs.h, line=1588 */
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*mremap)(struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
...
};

常見需要實現的pointer to function包含open(), read(), write(), close(), ioctl()…等,細節的部份我先不提。

主要來看ioctl()這件事,上面是4.14版本的code,看了一下的code發現並沒有我說的ioctl(),反而出現了unlocked_ioctl()compat_ioctl(),到底發生了什麼事情呢?

Big Kernel Lock下的舊產物: ioctl

ioctl()在2.6版本以前是還有這個function的,例如你可以在2.5.75的fs.h中看到。但在2.6以後就被替換成unlocked_ioctl()了,為什麼呢?

根據The new way of ioctl()這篇文章,

ioctl() is one of the remaining parts of the kernel which runs under the Big Kernel Lock (BKL). In the past, the usage of the BKL has made it possible for long-running ioctl() methods to create long latencies for unrelated processes. Recent changes, which have made BKL-covered code preemptible, have mitigated that problem somewhat. Even so, the desire to eventually get rid of the BKL altogether suggests that ioctl() should move out from under its protection.

由於ioctl()是早期仍然在BKL(Big Kernel Lock)機制下執行的產物(BKL是甚麼可以再去google,是kernel發展史中蠻有趣的一段歷史),BKL的機制會使得ioctl()的運作時間較長,可能會造成其他process的延遲。

隨著Kernel後續的改版,BKL不再是一個需要的機制了,大家開始把被BKL保護的function移除BKL。但一下子就把BKL完全移除還是會有顧慮,大家應該更加仔細的去審視是否有需要自己加入新的lock來保護自己的程序。所以此時需要一個過渡的機制: unlocked_ioctl()(by Michael s. Tsirkin),該function定義如下:

long (*unlocked_ioctl) (struct file *filp, unsigned int cmd, unsigned long arg);

如果某個驅動的fops提供了unlocked_ioctl(),那麼他將優先調用unlocked_ioctl()而不是有BKL版本的ioctl()

  • unlocked_ioctl()不再提供inode參數,但你仍可以透過filp->f_dentry->d_inode來取得
  • unlocked_ioctl()不再使用BKL,工程師需要根據自己的需求來決定要不要加入lock的機制

所以我們知道了原始unlocked_ioctl()的誕生是為了應付一段過渡期,讓大家能夠在這段時間趕快去修改自己的ioctl(),例如你在2.6.11版本的fs.h就可以看到這兩個接口是同時存在的。

  • 而在2.6.36後就正式將ioctl()移除了,大家都必須透過unlocked_ioctl()來提供ioctl的接口

不過這就只是一段歷史,對於現在的driver開發者也沒什麼影響,就是把自己寫好的ioctl接到unlocked_ioctl()上面去而已。

為了相容性而出現的compat_ioctl

在Michael s. Tsirkin發布的patch提供了unlocked_ioctl的同時也提供了另外一個接口: compat_ioctl()

If this method exists, it will be called (without the BKL) whenever a 32-bit process calls ioctl() on a 64-bit system. It should then do whatever is required to convert the argument to native data types and carry out the request

他出現的目的很簡單,就是相容性: 為了讓32-bit的process可以在64-bit上的system來執行ioctl()(沒有BKL版本)。

compat_ioctl()實際上要怎麼寫呢? 隨便找一個example code來看一下:

#ifdef CONFIG_COMPAT
static long debussy_compat_ioctl (struct file *filp, unsigned int cmd, unsigned long arg)
{
return debussy_ioctl(filp, cmd, (unsigned long)compat_ptr(arg));
}
#endif

static const struct file_operations debussy_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = debussy_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = debussy_compat_ioctl,
#endif
};

227
228
229
230
231
/* arch/arm64/include/asm/compat.h, line=227 */
static inline void __user *compat_ptr(compat_uptr_t uptr)
{
return (void __user *)(unsigned long)uptr;
}

會發現他實際上就是呼叫了原本的ioctl接口,只是將最後一個arg參數透過compat_ptr()轉換成64-bit了

總結

了解了這些會有什麼幫助嗎? 其實好像也還好xD

主要就是現在在寫fops時需要提供兩個接口:

  • unlock_ioctl()
  • compat_ioctl()

並且這不太會影響你原本的driver ioctl寫法(除非你會使用到inode),然後compat_ioctl的寫法也很制式化,如同上面的範例,將最後參數的指標轉換一下,呼叫你driver ioctl就好了。

References


>