linux內核空間跟用戶空間通信方法

linux驅動程序一般工作在內核空間,但也可以工作在用戶空間,內核空間和用戶空間之間如何進行通訊?

1、系統調用

read,write,ioctl 

A.get_user(x,ptr):在內核中被調用,獲取用戶空間指定地址的數值並保存到內核變量x中。

B.put_user(x,ptr):在內核中被調用,將內核空間的變量x的數值保存到到用戶空間指定地址處。

C.Copy_from_user()/copy_to_user():主要應用於設備驅動讀寫函數中,通過系統調用觸發。

2、虛擬文件系統 
proc文件系統 
sysfs文件系統 
debugfs文件系統

3、netlink

4、文件

5、信號

https://blog.csdn.net/gatieme/article/details/68948080

https://blog.csdn.net/vertor11/article/details/79622694

 

內核空間主動發起

https://blog.csdn.net/do2jiang/article/details/5509576

內核空間和用戶空間交換數據的方式有很多,比如用戶空間發起的系統調用、proc、虛擬文件系統等。

內核空間主動發起的有get_user/put_user、信號、netlink等。

這裏介紹get_user/put_user的使用以及背後的原理。

 

1. 創建module

要讓內核空間主動發起,需要創建一個module,然後插入到內核中。

從內核中發起創建kernel_file,並寫入內容。

最後從用戶空間進行驗證。

1.1 測試源碼

首先,編寫module源碼:

複製代碼

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

static char buf[] ="來自內核的訪問\n";
static char buf1[32];
 
int __init test_init(void)
{
    struct file *fp;
    mm_segment_t fs;
    loff_t pos;
    printk("test enter\n");
    fp =filp_open("/home/jenkins/lubaoquan/test/kernel_file",O_RDWR | O_CREAT,0644);
    if (IS_ERR(fp)){
        printk("create file error\n");
        return -1;
    }
    fs =get_fs();
    set_fs(KERNEL_DS);
    pos =0;
    vfs_write(fp,buf, sizeof(buf), &pos);
    pos =0;
    vfs_read(fp,buf1, sizeof(buf), &pos);
    printk("Write contet=%s\n",buf1);
    filp_close(fp,NULL);
    set_fs(fs);
    return 0;
}
void __exit test_exit(void)
{
    printk("test exit\n");
}
 
module_init(test_init);
module_exit(test_exit);
 
MODULE_LICENSE("GPL");

複製代碼

 

 編寫Makefile文件:

複製代碼

obj-m :=read_userspace.o                        #要生成的模塊名     
read_userspace-objs:= read_userspace_file.o     #生成這個模塊名所需要的目標文件

KDIR := /lib/modules/`uname -r`/build
PWD := $(shell pwd)

default:
    make -C $(KDIR) M=$(PWD) modules

clean:
    rm -rf *.o *.cmd *.ko *.mod.c .tmp_versions Module.symvers modules.order

複製代碼

 

1.2 編譯

執行make命令,就可以得到read_userspace.ko文件。

1.3 測試

sudo insmod read_userspace.ko-----------------插入模組

sudo lsmod | grep read_userspace--------------驗證是否插入成功

sudo rmmod read_userspace----------------------移除模組

測試結果如下,可以看出kernel_file是由root用戶創建的。

可以看出內容符合預期。

 

3. 代碼分析

 

複製代碼

    fp =filp_open("/home/jenkins/lubaoquan/test/kernel_file",O_RDWR | O_CREAT,0644);---------------------創建用戶空間文件,獲取文件句柄。
    if (IS_ERR(fp)){
        printk("create file error\n");
        return -1;
    }
    fs =get_fs();----------------------------------------------------------------------------------------獲取當前線程的thread_info->addr_limit。
    set_fs(KERNEL_DS);-----------------------------------------------------------------------------------將能訪問的空間thread_info->addr_limit擴大到KERNEL_DS。
    pos =0;
    vfs_write(fp,buf, sizeof(buf), &pos);----------------------------------------------------------------調用vfs_write寫內容
    pos =0;
    vfs_read(fp,buf1, sizeof(buf), &pos);----------------------------------------------------------------調用vfs_read讀取內容
    printk("Write contet=%s\n",buf1);
    filp_close(fp,NULL);---------------------------------------------------------------------------------關閉文件
    set_fs(fs);------------------------------------------------------------------------------------------將thread_info->addr_limit切換回原來值

複製代碼

 

 

4. 原理

 

4.1 set_fs和get_fs

有下面代碼可知KERNEL_DS範圍很大,到0xffffffffffffffff。

而USER_DS範圍較小,到0x7ffffffff000。

由Linux內存分佈圖可知,KERNEL_DS意味着可以訪問整個內存所有空間,USER_DS只能訪問用戶空間內存。

通過set_fs可以改變thread_info->addr_limit的大小。

複製代碼

/*
 * For historical reasons, the following macros are grossly misnamed:
 */
#define KERNEL_DS    ((mm_segment_t) { ~0UL })        /* cf. access_ok() */
#define USER_DS        ((mm_segment_t) { TASK_SIZE-1 })    /* cf. access_ok() */

#define VERIFY_READ    0
#define VERIFY_WRITE    1

#define get_ds()  (KERNEL_DS)
#define get_fs()  (current_thread_info()->addr_limit)
#define set_fs(x) (current_thread_info()->addr_limit = (x))


#define TASK_SIZE           DEFAULT_TASK_SIZE 

複製代碼

 

4.2 vfs_write和vfs_read對addr_limit的檢查

將代碼修改一下,不進行addr_limit擴大,看看結果如何。

複製代碼

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

static char buf[] ="來自內核的訪問\n";
static char buf1[32];
 
int __init test_init(void)
{
    struct file *fp;
    mm_segment_t fs;
    loff_t pos;
    int ret;
    
    printk("KERNEL_DS=0x%llx USER_DS=0x%llx get_fs()=0x%llx\n", KERNEL_DS, USER_DS, get_fs());
    fp =filp_open("/home/jenkins/lubaoquan/test/kernel_file",O_RDWR | O_CREAT,0644);
    if (IS_ERR(fp)){
        printk("create file error\n");
        return -1;
    }
    fs =get_fs();
    //set_fs(KERNEL_DS);
    pos =0;
    printk("fp=%p, buf=%p get_fs()=0x%llx\n", fp, buf, get_fs());
    ret = vfs_write(fp,buf, sizeof(buf), &pos);
    printk("ret=%d\n", ret);
    pos =0;
    printk("fp=%p, buf1=%p\n", fp, buf1);
    ret = vfs_read(fp,buf1, sizeof(buf), &pos);
    printk("ret=%d Write contet=%s\n", ret, buf1);
    filp_close(fp,NULL);
    //set_fs(fs);
    return 0;
}
void __exit test_exit(void)
{
    printk("test exit\n");
}
 
module_init(test_init);
module_exit(test_exit);
 
MODULE_LICENSE("GPL");

複製代碼

 

 

執行結果如下,可以看出fp、buf、buf1都位於內核空間。而當前空間的get_fs()爲0x7ffffffff000,這些地址都超出當前空間。

所以vfs_read和vfs_write返回值都是-14,即“Bad address”。

複製代碼

[49001.240705] KERNEL_DS=0xffffffffffffffff USER_DS=0x7ffffffff000 get_fs()=0x7ffffffff000
[49001.240713] fp=ffff8800cae06900, buf=ffffffffc0305000 get_fs()=0x7ffffffff000
[49001.240714] ret=-14
[49001.240715] fp=ffff8800cae06900, buf1=ffffffffc03053c0
[49001.240716] ret=-14 Write contet=
[49013.464812] test exit

複製代碼

 

 

簡單看一下vfs_write和vfs_read,兩者都調用access_ok對地址合法性進行檢查,嚴禁addr大於當前get_fs()。

此處buf和buf1都不滿足條件,所以返回-EFAULT。

複製代碼

#define __access_ok(addr, size, segment)                        \
({                                            \
    __chk_user_ptr(addr);                                \
    (likely((unsigned long) (addr) <= (segment).seg)                \
     && ((segment).seg == KERNEL_DS.seg                        \
         || likely(REGION_OFFSET((unsigned long) (addr)) < RGN_MAP_LIMIT)));    \
})
#define access_ok(type, addr, size)    __access_ok((addr), (size), get_fs())


ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{
...
    if (unlikely(!access_ok(VERIFY_READ, buf, count)))
        return -EFAULT;
...
}

ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
...
    if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))
        return -EFAULT;
...
}

複製代碼

 

將測試代碼紅色部分打開,擴大addr_limit空間。

可以看出當前thread_info->addr_limit變成了0xffffffffffffffff。

所以vfs_write和vfs_read的access_ok檢查得以通過,程序得到正確執行。

複製代碼

[48937.547119] KERNEL_DS=0xffffffffffffffff USER_DS=0x7ffffffff000 get_fs()=0x7ffffffff000
[48937.547138] fp=ffff8800c8300c00, buf=ffffffffc02f3000 get_fs()=0xffffffffffffffff
[48937.547155] ret=23
[48937.547158] fp=ffff8800c8300c00, buf1=ffffffffc02f33c0
[48937.547164] ret=23 Write contet=\xffffffe6\xffffff9d\xffffffa5\xffffff9d\xffffffa5\xffffffe8\xffffff87\xffffffaa\xffffff87\xffffffaa\xffffffe5\xffffff86\xffffff85\xffffff86\xffffff85\xffffffe6\xffffffa0\xffffffb8\xffffffa0\xffffffb8\xffffffe7\xffffff9a\xffffff84\xffffff9a\xffffff84\xffffffe8\xffffffae\xffffffbf\xffffffae\xffffffbf\xffffffe9\xffffff97\xffffffae\xffffff97\xffffffae
[48937.547164] 
[48940.600703] test exit

複製代碼

  

5. 小結

只有使用上面的方法,才能在內核中使用open,write等的系統調用。

其實這樣做的主要原因是open,write的參數在用戶空間,在這些系統調用的實現裏需要對參數進行檢查,就是檢查它的參數指針地址是不是用戶空間的。

系統調用本來是提供給用戶空間的程序訪問的,所以,對傳遞給它的參數(比如上面的buf、buf1),它默認會認爲來自用戶空間。

在vfs_write()函數中,爲了保護內核空間,一般會用get_fs()得到的值來和USER_DS進行比較,從而防止用戶空間程序“蓄意”破壞內核空間。

爲了解決這個問題, set_fs(KERNEL_DS)將其能訪問的空間限制擴大到KERNEL_DS,這樣就可以在內核順利使用系統調用了!

 

內核使用系統調用參數肯定是內核空間,爲了不讓這些系統調用檢查參數所以必須設置  set_fs(KERNEL_DS)才能使用該系統調用。

vfs_write的流程可調用access_ok,而access_ok會判斷訪問的buf是否在0~addr_limit之間,如何是就ok;否則-EFAULT,這顯然是爲用戶準備的檢查。

addr_limit一般設爲USER_DS,在內核空間,buf肯定>USER_DS,必須修改addr_limit,這就是set_fs的由來。

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章