作者 [email protected] 彭東林 平臺 busybox-1.24.2 Linux-4.10.17 Qemu+vexpress-ca9 概述 在寫驅動的時候,我們經常會向用戶空間導出一些文件,然後用戶空間使用cat命令去讀取該節點,從而完成kernel跟user的通信。但是有 ...
作者
[email protected] 彭東林平臺
busybox-1.24.2 Linux-4.10.17 Qemu+vexpress-ca9概述
在寫驅動的時候,我們經常會向用戶空間導出一些文件,然後用戶空間使用cat命令去讀取該節點,從而完成kernel跟user的通信。但是有時會發現,如果節點對應的read回調函數寫的有問題的話,使用cat命令後,節點對應的read函數會被頻繁調用,log直接刷屏,而我們只希望read被調用一次,echo也是一樣的道理。背後的原因是什麼呢?如何解決呢?下麵我們以debugfs下的節點讀寫為例說明一下。正文
一、read和write的介紹
1、系統調用 read ssize_t read(int fd, void *buf, size_t count);這個函數會從fd表示的文件描述符中讀取count個位元組到buf緩衝區當中,返回值有下麵幾種: 如果返回值大於0,表示實際讀到的位元組數,返回0的話,表示讀到了文件結尾,同時文件的file position也會被更新。實際讀到的位元組數可能會比count小。 如果返回-1,表示讀取失敗,errno會被設置為相應的值。 2、系統調用 write ssize_t write(int fd, const void *buf, size_t count); 這個函數將以buf為首地址的緩衝區當中的count個位元組寫到文件描述符fd表示的文件當中,返回值: 返回正整數,表示實際寫入的位元組數,返回0表示沒有任何東西被寫入,同時文件位置指針也會被更新 返回-1,表示寫失敗,同時errno會被設置為相應的值 3、LDD3上對驅動中實現的read回調函數的解釋 原型: ssize_t (*read) (struct file *fp, char __user *user_buf, size_t count, loff_t *ppos); fp 被打開的節點的文件描述符 user_buf表示的是用戶空間的一段緩衝區的首地址,從kernel讀取的數據需要存放該緩衝區當中 count表示用戶期望讀取的位元組數 *ppos表示當前當前文件位置指針的大小,這個值會需要驅動程式自己來更新,初始大小是0 如果返回值等於傳遞給read系統調用的count參數,則說明所請求的位元組數傳輸成功完成。這是最理想的情況 如果返回值是正的,但是比count小,則說明只有部分數據傳輸成功。這種情況下因設備的不同可能有許多原因。大部分情況下,程式會再次讀數據。例如,如果用fread函數讀數據,這個庫函數就會不斷調用系統調用,直至所請求的數據傳輸完畢為止 如果返回值為0,則表示已經達到了文件尾 負值意味著發生了錯誤,該值指明瞭發生了什麼錯誤,錯誤碼在<linux/errno.h>中定義。比如這樣的一些錯誤:-EINTR(系統調用被中斷)或者-EFAULT(無效地址) 4、LDD3上對驅動中實現的write回調函數的解釋 原型: ssize_t (*write) (struct file *fp, const char __user *user_buf, size_t count, loff_t *ppos); fp:被打開的要寫的內核節點的文件描述符 user_buf:表示的是用戶空間的一段緩衝區的首地址,其中存放的是用戶需要傳遞給kernel的數據 count:用戶期望寫給kernel的位元組數 *ppos:文件位置指針,需要驅動程式自己更新 如果返回值等於count,則完成了所請求數目的位元組傳輸 如果返回值為正的,但小於count,則這傳輸了部分數據。程式很可能再次試圖寫入餘下的數據 如果返回值為0,意味著什麼也沒有寫入。這個結果不是錯誤,而且也沒有理由返回一個錯誤碼。再次重申,標準庫會重覆調用write 負值意味著發生了錯誤,與read相同,有效的錯誤碼定義在<linux/errno.h>中 上面加粗的紅色字體引起驅動中的write或者read被反覆調用的原因。
二、簡略的分析一下read和write系統調用的實現
在用戶空間調用read函數後,內核函數vfs_read會被調用:1 ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos) 2 { 3 ssize_t ret; 4 5 if (!(file->f_mode & FMODE_READ)) 6 return -EBADF; 7 if (!(file->f_mode & FMODE_CAN_READ)) 8 return -EINVAL; 9 if (unlikely(!access_ok(VERIFY_WRITE, buf, count))) 10 return -EFAULT; 11 12 ret = rw_verify_area(READ, file, pos, count); 13 if (!ret) { 14 if (count > MAX_RW_COUNT) 15 count = MAX_RW_COUNT; 16 ret = __vfs_read(file, buf, count, pos); 17 if (ret > 0) { 18 fsnotify_access(file); 19 add_rchar(current, ret); 20 } 21 inc_syscr(current); 22 } 23 24 return ret; 25 }下麵是需要關註的: 第9行檢查用戶空間的buf緩衝區是否可以寫入 第14行檢查count的大小,這裡MAX_RW_COUNT被設置為1個頁的大小,這裡的值是4KB,也就是一次用戶一次read最多獲得4KB數據 第16行調用__vfs_read,這個函數最終會調用到我們的驅動中的read函數,可以看到這個函數的參數跟驅動中的read函數一樣,驅動中read返回的數字ret會返回給用戶,這裡並沒有看到更新pos,所以需要在我們的驅動中自己去更新。 用戶空間調用write函數後,內核函數vfs_write會被調用:
1 ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos) 2 { 3 ssize_t ret; 4 5 if (!(file->f_mode & FMODE_WRITE)) 6 return -EBADF; 7 if (!(file->f_mode & FMODE_CAN_WRITE)) 8 return -EINVAL; 9 if (unlikely(!access_ok(VERIFY_READ, buf, count))) 10 return -EFAULT; 11 12 ret = rw_verify_area(WRITE, file, pos, count); 13 if (!ret) { 14 if (count > MAX_RW_COUNT) 15 count = MAX_RW_COUNT; 16 file_start_write(file); 17 ret = __vfs_write(file, buf, count, pos); 18 if (ret > 0) { 19 fsnotify_modify(file); 20 add_wchar(current, ret); 21 } 22 inc_syscw(current); 23 file_end_write(file); 24 } 25 26 return ret; 27 }
這裡需要關註:
第9行,檢查用戶空間的緩衝區buf是否可以讀 第15行,限制一次寫入的數據最多為1頁,比如4KB 第17行的_vfs_write的參數跟驅動中的write的參數一樣,__vfs_write的返回值ret也就是用戶調用write時的返回值,表示實際寫入的位元組數,這裡也沒有看到更新pos的代碼,所以需要我們自己在驅動的write中實現三、簡略分析cat和echo的實現
由於使用的根文件系統使用busybox做的,所以cat和echo的實現在busybox的源碼中,如下: coreutils/cat.c coreutils/echo.c CAT: 下麵簡略分析cat的實現,cat的預設實現採用了sendfile,採用sendfile可以減少不必要的記憶體拷貝,從而提高讀寫效率,這就是所謂的Linux的“零拷貝”。為了便於代碼分析,可以關閉這個功能,然後cat就會調用read和write實現了: Busybox Settings ---> General Configuration ---> [ ] Use sendfile system call 下麵是cat的核心函數: 以 cat xxx為例其中src_fd就是被打開的內核節點的文件描述符,dst_fd就是標準輸出描述符,size是01 static off_t bb_full_fd_action(int src_fd, int dst_fd, off_t size) 2 { 3 int status = -1; 4 off_t total = 0; 5 bool continue_on_write_error = 0; 6 ssize_t sendfile_sz; 7 char buffer[4 * 1024]; // 用戶空間緩衝區,4KB大小 8 enum { buffer_size = sizeof(buffer) }; // 每次read期望獲得的位元組數 9 10 sendfile_sz = 0; 11 if (!size) { 12 size = (16 * 1024 *1024); // 剛開始,如傳入的size是0,這裡將size設置為16MB 13 status = 1; /* 表示一直讀到文件結尾,也就是直到read返回0 */ 14 } 15 16 while (1) { 17 ssize_t rd; 18 19 rd = safe_read(src_fd, buffer, buffer_size); // 這裡調用的就是read, 讀取4KB,rd是實際讀到的位元組數 20 if (rd < 0) { 21 bb_perror_msg(bb_msg_read_error); 22 break; 23 } 24 read_ok: 25 if (!rd) { /* 表示讀到了文件結尾,那麼結束迴圈 */ 26 status = 0; 27 break; 28 } 29 /* 將讀到的內容輸出到dst_fd表示的文件描述符 */ 30 if (dst_fd >= 0 && !sendfile_sz) { 31 ssize_t wr = full_write(dst_fd, buffer, rd); 32 if (wr < rd) { 33 if (!continue_on_write_error) { 34 bb_perror_msg(bb_msg_write_error); 35 break; 36 } 37 dst_fd = -1; 38 } 39 } 40 41 total += rd; // total記錄的是讀到的位元組數的累計值 42 if (status < 0) { /* 如果傳入的size不為0,那麼status為-1,直到讀到size個位元組後,才會退出。如果size為0,這個條件不會滿足 */ 43 size -= rd; 44 if (!size) { 45 /* 'size' bytes copied - all done */ 46 status = 0; 47 break; 48 } 49 } 50 } 51 out: 52 return status ? -1 : total; // 當讀完畢,status為0,這裡返回累計讀到的位元組數 53 }
從上面的分析我們知道如下信息:
使用cat xxx時,上面的函數傳入的size為0,那麼上面的while迴圈會一直進行read,直到出錯或者read返回0,read返回0也就是讀到文件結尾。最後如果出錯,那麼返回-1,否則的話,返回讀到的累計的位元組數。 到這裡,應該就是知道為什麼驅動中的read會被頻繁調用了吧,也就是驅動中的read的返回值有問題。 ECHO: echo的核心函數是full_write 這裡fd是要寫的內核節點,buf緩衝區中存放的是要寫入的內容,len是buf緩衝區中存放的位元組數1 ssize_t FAST_FUNC full_write(int fd, const void *buf, size_t len) 2 { 3 ssize_t cc; 4 ssize_t total; 5 6 total = 0; 7 8 while (len) { 9 cc = safe_write(fd, buf, len); 10 11 if (cc < 0) { 12 if (total) { 13 /* we already wrote some! */ 14 /* user can do another write to know the error code */ 15 return total; 16 } 17 return cc; /* write() returns -1 on failure. */ 18 } 19 20 total += cc; 21 buf = ((const char *)buf) + cc; 22 len -= cc; 23 } 24 25 return total; 26 }
上面的函數很簡單,可以得到如下信息:
如果write的函數返回值cc小於len的話,會一直調用write,直到報錯或者len個位元組全部寫完。而這裡的cc對應的就是我們的驅動中write的返回值。最後,返回實際寫入的位元組數或者一個錯誤碼。 到這裡,應該也已經清除為什麼調用一次echo後,驅動的write為什麼會被頻繁調用了吧,還是驅動中write的返回值的問題。 知道的上面的原因,下麵我們結合一個簡單的驅動看看。四、實例分析
1、先看兩個刷屏的例子
這個驅動在/sys/kernel/debug生成一個demo節點,支持讀和寫。
1 #include <linux/init.h> 2 #include <linux/module.h> 3 #include <linux/debugfs.h> 4 #include <linux/fs.h> 5 #include <asm/uaccess.h> 6 7 static struct dentry *demo_dir; 8 9 static ssize_t demo_read(struct file *fp, char __user *user_buf, size_t count, loff_t *ppos) 10 { 11 char kbuf[10]; 12 int ret, wrinten; 13 14 printk(KERN_INFO "user_buf: %p, count: %d, ppos: %lld\n", 15 user_buf, count, *ppos); 16 17 wrinten = snprintf(kbuf, 10, "%s", "Hello"); 18 19 ret = copy_to_user(user_buf, kbuf, wrinten+1); 20 if (ret != 0) { 21 printk(KERN_ERR "read error"); 22 return -EIO; 23 } 24 25 *ppos += wrinten; 26 27 return wrinten; 28 } 29 30 static ssize_t demo_write (struct file *fp, const char __user *user_buf, size_t count, loff_t *ppos) 31 { 32 char kbuf[10] = {0}; 33 int ret; 34 35 printk(KERN_INFO "user_buf: %p, count: %d, ppos: %lld\n", 36 user_buf, count, *ppos); 37 38 ret = copy_from_user(kbuf, user_buf, count); 39 if (ret) { 40 pr_err("%s: write error\n", __func__); 41 return -EIO; 42 } 43 44 *ppos += count; 45 46 return 0; 47 } 48 49 static const struct file_operations demo_fops = { 50 .read = demo_read, 51 .write = demo_write, 52 }; 53 54 static int __init debugfs_demo_init(void) 55 { 56 int ret = 0; 57 58 demo_dir = debugfs_create_file("demo", 0444, NULL, 59 NULL, &demo_fops); 60 61 return ret; 62 } 63 64 static void __exit debugfs_demo_exit(void) 65 { 66 if (demo_dir) 67 debugfs_remove(demo_dir); 68 } 69 70 module_init(debugfs_demo_init); 71 module_exit(debugfs_demo_exit); 72 MODULE_LICENSE("GPL");
我們先來看看運行結果:
先試試寫: [root@vexpress mnt]# echo 1 > /d/demo 執行這個命令並不會返回,會卡主,再看看kernel log,已經刷屏: [ 1021.547015] user_buf: 00202268, count: 2, ppos: 0 [ 1021.547181] user_buf: 00202268, count: 2, ppos: 2 [ 1021.547319] user_buf: 00202268, count: 2, ppos: 4 [ 1021.547466] user_buf: 00202268, count: 2, ppos: 6 .... .... [ 1022.008736] user_buf: 00202268, count: 2, ppos: 6014 [ 1022.008880] user_buf: 00202268, count: 2, ppos: 6016 [ 1022.009012] user_buf: 00202268, count: 2, ppos: 6018 ... ... 再試試讀: [root@vexpress mnt]# cat /d/demo HelloHelloHelloHelloHelloHelloHelloHelloHelloHelloHello... ... 可以看到,終端被Hello填滿了,再看看kernel log,刷屏了: [ 1832.074616] user_buf: becb6be8, count: 4096, ppos: 0 [ 1832.075033] user_buf: becb6be8, count: 4096, ppos: 5 [ 1832.075240] user_buf: becb6be8, count: 4096, ppos: 10 [ 1832.075898] user_buf: becb6be8, count: 4096, ppos: 15 [ 1832.076093] user_buf: becb6be8, count: 4096, ppos: 20 [ 1832.076282] user_buf: becb6be8, count: 4096, ppos: 25 [ 1832.076468] user_buf: becb6be8, count: 4096, ppos: 30 [ 1832.076653] user_buf: becb6be8, count: 4096, ppos: 35 [ 1832.076841] user_buf: becb6be8, count: 4096, ppos: 40 ... ... 可以看到規律,對於write,每次的count都是2,因為寫下來的是個字元串的"1",ppos以2為臺階遞增。此外,可以看到user_buf每次都相同,結合echo源碼可以發現,用戶的user_buf是在堆上分配的,所以地址比較小 對於read,每次要讀的count都是4KB,ppos是以5為臺階遞增,正好是strlen("Hello"),user_buf的值每次都相同,結合cat源碼可以發現,用戶的user_buf是在棧上分配的,所以地址比較大 下圖是x86系統下Linux進程的進程地址空間的記憶體佈局,這是只是說明一下意思。 下麵開始分別針對write和read進行修改:2、對write進行修改
write版本2: 既然經過前面的分析,知道write被頻繁調用的原因是用戶調用write實際寫入的位元組數小於期望的,而用戶的write的返回值來自驅動的write,那麼我們直接然write返回count不就可以了嗎。1 static ssize_t demo_write (struct file *fp, const char __user *user_buf, size_t count, loff_t *ppos) 2 { 3 char kbuf[10] = {0}; 4 int ret; 5 6 printk(KERN_INFO "user_buf: %p, count: %d, ppos: %lld\n", 7 user_buf, count, *ppos); 8 9 ret = copy_from_user(kbuf, user_buf, count); 10 if (ret) { 11 pr_err("%s: write error\n", __func__); 12 return -EIO; 13 } 14 15 *ppos += count; 16 17 return count; 18 }
驗證:
[root@vexpress mnt]# echo 1 > /d/demo 敲完回車後,立馬就返回了,kernel log也只列印了一次: [ 2444.363351] user_buf: 00202408, count: 2, ppos: 0 write版本3: 其實,kernel提供了一個很方便的函數,simple_write_to_buffer,這個函數專門完成從user空間向kernel空間拷貝數據:1 static ssize_t demo_write (struct file *fp, const char __user *user_buf, size_t count, loff_t *ppos) 2 { 3 char kbuf[10] = {0}; 4 5 printk(KERN_INFO "user_buf: %p, count: %d, ppos: %lld\n", 6 user_buf, count, *ppos); 7 8 return simple_write_to_buffer(kbuf, sizeof(kbuf), ppos, user_buf, count); 9 }驗證: [root@vexpress mnt]# echo 1 > /d/demo 敲完回車後,立馬就返回了,kernel log也只列印了一次: [ 2739.984844] user_buf: 00202340, count: 2, ppos: 0 簡單看看simple_write_to_buffer的實現:
1 /** 2 * simple_write_to_buffer - copy data from user space to the buffer 3 * @to: the buffer to write to 4 * @available: the size of the buffer 5 * @ppos: the current position in the buffer 6 * @from: the user space buffer to read from 7 * @count: the maximum number of bytes to read 8 * 9 * The simple_write_to_buffer() function reads up to @count bytes from the user 10 * space address starting at @from into the buffer @to at offset @ppos. 11 * 12 * On success, the number of bytes written is returned and the offset @ppos is 13 * advanced by this number, or negative value is returned on error. 14 **/ 15 ssize_t simple_write_to_buffer(void *to, size_t available, loff_t *ppos, 16 const void __user *from, size_t count) 17 { 18 loff_t pos = *ppos; 19 size_t res; 20 21 if (pos < 0) 22 return -EINVAL; 23 if (pos >= available || !count) 24 return 0; 25 if (count > available - pos) 26 count = available - pos; 27 res = copy_from_user(to + pos, from, count); 28 if (res == count) 29 return -EFAULT; 30 count -= res; 31 *ppos = pos + count; 32 return count; 33 } 34 EXPORT_SYMBOL(simple_write_to_buffer);
可以看到,最後返回的是count,如果copy_from_user沒都拷貝全,將來write還是會被再次調用。
3、對read進行修改
我們知道read被返回調用的原因是,read返回的值小於用戶期望讀取的值,對於這裡,就是4KB。而對於cat來說,每次read都期望獲取4KB的數據,而且在不考慮出錯的情況下,只有read返回0,cat才會終止。 read版本2:1 static ssize_t demo_read(struct file *fp, char __user *user_buf, size_t count, loff_t *ppos) 2 { 3 char kbuf[10]; 4 int ret, wrinten; 5 6 printk(KERN_INFO "user_buf: %p, count: %d, ppos: %lld\n", 7 user_buf, count, *ppos); 8 9 wrinten = snprintf(kbuf, 10, "%s", "Hello"); 10 11 ret = copy_to_user(user_buf, kbuf, wrinten+1); 12 if (ret != 0) { 13 printk(KERN_ERR "read error"); 14 return -EIO; 15 } 16 17 *ppos += wrinten; 18 19 return 0; 20 }驗證: [root@vexpress mnt]# cat /d/demo 執行回車後,"Hello"卻沒有輸出,但是驅動的read驅動被調用了一次: [ 118.837456] user_buf: beeb0be8, count: 4096, ppos: 0 這是什麼原因呢?可以看看cat的核心函數bb_full_fd_action,其中,如果read返回0,並不會將讀到的內容輸出到標準輸出上,所以cat的時候什麼都沒看到。 既然返回0不行,那麼返回count,也就是用戶期望的4KB,行不行呢? read版本3:
1 static ssize_t demo_read(struct file *fp, char __user *user_buf, size_t count, loff_t *ppos) 2 { 3 char kbuf[10]; 4 int ret, wrinten; 5 6 printk(KERN_INFO "user_buf: %p, count: %d, ppos: %lld\n", 7 user_buf, count, *ppos); 8 9 wrinten = snprintf(kbuf, 10, "%s", "Hello"); 10 11 ret = copy_to_user(user_buf, kbuf, wrinten+1); 12 if (ret != 0) { 13 printk(KERN_ERR "read error"); 14 return -EIO; 15