關注“技術簡說”,帶你一步一步學習linux內核驅動。
在linux操作系統中,一切皆是文件:文件是文件,目錄是文件,設備是文件,socket套接字是文件,管道也是文件。
linux操作系統用文件抽象出了這一切,文件成為了以上這些實體的編程接口。正由于此,基于linux的編程變成了面向文件的編程,對于linux應用程序開發者而言,簡直是爽的不要不要的。
但是,對于內核開發者而言,卻是未必。雖然應用層可以用open, write,read操縱一切,但是在內核里面,卻需要不同的部分(或者說驅動)來真正實現這一切。
本文接著linux驅動開發第1講:帶你編寫一個最簡單的字符設備驅動,來講述linux應用程序中的write()函數如何調用到hello驅動里的write()函數,并順道回答上一講最后的幾個遺留問題。
先上一張圖簡單說明下調用流程:
任何一個可以正常使用的函數,如果你的應用程序里沒有定義,那么肯定定義在c庫里。而c庫怎么做會視情況而定。像一些字符處理函數,c庫里會實現它們;但是像write函數,c庫只會做一些檢查,然后就陷入write的系統調用,系統調用會通過軟中斷的方式陷入到內核空間里去執行。
應用空間和內核空間是彼此隔離的,互相看不到對方,也無法訪問對方的數據,這是為了安全。所以就write函數而言,當從用戶空間通過系統調用進入到內核空間以后,內核需要通過copy_from_user函數才能把應用程序傳給write的數據拷貝到內核空間,之后內核空間才能對此數據進行處理。
整個流程,上圖表現的已經非常明顯,但是問題也是有的,操作系統中的系統調用最終是如何知道應該調用哪個驅動里的write函數呢?在linux驅動開發第1講:帶你編寫一個最簡單的字符設備驅動里,我們確實看到當執行測試程序的時候,當調用測試程序里的open, write和read函數的時候,分別調用到了hello驅動里的hello_open, hello_write和hello_read函數,難道這是一個巧合嗎?
當然不是!
我們先從邏輯上說明這個問題。
如果我們沒有記錯,在hello驅動里,有定義主次設備號:
int reg_major = 232;
int reg_minor = 0;
int hello_init(void)
{
devNum = MKDEV(reg_major, reg_minor);
gDev = kzalloc(sizeof(struct cdev), GFP_KERNEL);
gFile = kzalloc(sizeof(struct file_operations), GFP_KERNEL);
...
cdev_init(gDev, gFile);
cdev_add(gDev, devNum, 3);
}
在hello_init里,我們把主設備號232和此設備號0組合成了devNum。
cdev_init(gDev, gFile); 建立了gDev和gFile的邏輯關系;
cdev_add(gDev, devNum, 3); 建立了gDev和devNum的邏輯關系;
其實你翻開代碼看細節會發現,以上兩句代碼其實建立了gFile和devNum的對應關系,也就是file_operations和devNum的對應關系,也就是建立了file_operation和主次設備號(232,0)的對應關系。
注意:在linux里,在應用層用文件句柄也就是fd表示一個打開的文件,但是在內核里用struct file 表示一個打開的文件,用struct file_operations表示對該文件的操作。fd和struct file是一一對應的,而struct file和struct file_operations也是一一對應的。這是struct file_operations的結構體定義:
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 *);
int (*mmap) (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 *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
...
};
在上一講的例子里,我們打開的文件名字是/dev/hello,這是一個設備文件,對應的主次設備號分別為232和0。所以,當你打開/dev/hello之后,就已經建立了這個文件和hello驅動里的 struct file 的對應關系,也就建立了這個文件和hello驅動里的struct file_operations的對應關系。
好,了解以上的背景之后,我們來看看代碼。
我們從內核里write系統調用的實現部分開始閱讀:
相關的代碼在:fs/read_write.c
備注:SYSCALL_開頭表示是系統調用。
關鍵代碼在vfs_write。所以,我們繼續跟進入:
繼續跟入__vfs_write:
ssize_t __vfs_write(struct file *file, const char __user *p, size_t count,
loff_t *pos)
{
if (file->f_op->write)
return file->f_op->write(file, p, count, pos);
else if (file->f_op->write_iter)
return new_sync_write(file, p, count, pos);
else
return -EINVAL;
關鍵代碼在這里:
if (file->f_op->write)
return file->f_op->write(file, p, count, pos);
上面提到建立了/dev/hello和file_operations的關系。所以這里其實就是判斷hello驅動里有沒有定義write函數,如果有,那就調用hello驅動里的write函數。
所以,按照如上的路徑,應用程序里的write就順利的調用到了hello驅動里的write函數。因為我們驅動里的hello_write和hello_read里都返回了0。所以,應用程序里的write和read也返回了0。
ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l)
{
printk(KERN_EMERG"hello_writern");
return 0;
}
ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l)
{
printk(KERN_EMERG"hello_readrn");
return 0;
}
如果你想讓測試程序里的write和read返回非零值,只要把驅動里的return 0,改為任意值就好了,大家可以自己測試一下。
關注“技術簡說”,帶你一步一步學習linux內核驅動。