1. 程式人生 > >20 字元裝置驅動相關的函式和引數及實現(虛擬檔案)

20 字元裝置驅動相關的函式和引數及實現(虛擬檔案)

字元裝置驅動相關的函式和引數及實現(虛擬檔案)


使用者程序呼叫函式順序:

open   ---> kernel ---> cdev.ops->open(..)
read   ---> kernel ---> cdev.ops->read(..)
ioctl  ---> kernel ---> cdev.ops->unlocked_ioctl(..)

裝置驅動實現好功能後,基本上由使用者程序通過系統呼叫後進來呼叫的。

如需要迴圈操作硬體,則應是在使用者程序裡迴圈。

使用者程序操作裝置驅動,網路通訊等與普通的文字檔案操作的程式設計介面基本一樣(open, read, write, ioctl …),這套介面就是所謂的VFS(虛擬檔案系統)。


當用戶程序open裝置檔案時,核心會根據開啟的裝置檔案的裝置號找到對應的cdev物件,檢查cdev.ops->open,如果不為空,則呼叫驅動裡的open函式,如果為空,核心直接返回fd。

注意:使用者程序開啟裝置檔案得到檔案描述符不是由裝置驅動裡指定的,裝置驅動裡的open函式僅僅是告訴核心是否已正常開啟。


在linux核心,用一個inode節點物件描述一個要操作的檔案/裝置檔案,包括許可權,裝置號等資訊。

一個檔案可以開啟很多次,但都是共用一個inode物件來描述屬性的。

檔案描述符屬於一個程序的資源,不同程序裡有可能相同的檔案描述符。

struct inode {
    ...
dev_t i_rdev; //裝置檔案對應的裝置號,驅動裡即可通過區分次裝置號來區別不同的具體硬體 struct cdev *i_cdev; //指向對應的字元裝置驅動cdev物件的地址 ... };

在使用者程序裡用一個int型別來表示檔案描述符,但檔案描述符裡有還存有對檔案位置的偏移,開啟標誌等資訊,用一個int數無法記錄下來的,所在每個檔案描述符的資訊都是由核心裡用file物件描述檔案描述符,在檔案開啟時建立,關閉時銷燬。

struct file {
    ...
    struct path     f_path;
    const struct file_operations    *f_op; //對應的檔案操作物件的地址
    unsigned int        f_flags; //檔案開啟的標誌
    fmode_t         f_mode; //許可權
    loff_t          f_pos;  //檔案描述符的偏移
    struct fown_struct  f_owner; //屬於哪個程序
    unsigned int        f_uid, f_gid; 
    void            *private_data; //給驅動程式設計師使用
    ...
};

如果開啟裝置檔案,那麼得到的file物件:

file物件裡的成員f_path.dentry->d_inode->i_rdev可以獲取到裝置檔案的裝置號
file物件裡的成員f_path.dentry->d_inode可以獲取到裝置檔案的inode物件的地址

注意:一個檔案只有一個inode節點物件,但是可以開啟多次,得到不同的檔案描述符物件(也就是多個struct file物件)。

虛擬檔案系統實現相關函式:

struct file_operations裡函式引數:

int (*open) (struct inode *, struct file *);
    //inode表示應用程式開啟的檔案的節點物件,file表示開啟檔案獲取到的檔案描述符
    //返回值0表示成功開啟,負數表示開啟失敗
    //核心根據open函式的返回值來確定是否給呼叫的使用者程序分配檔案描述符
    //在驅動可以不實現此函式,如不實現,則表示每次開啟都是成功的

ssize_t (*read) (struct file *fl, char __user *buf, size_t len, loff_t *off);
    //buf指向使用者程序裡的緩衝區,len表示buf的大小(由使用者呼叫read時傳進來的)
    //off表示fl檔案描述符的操作偏移,返回值為實際給使用者的資料位元組數
    //注意:必須通過off指標來改變檔案描述符的偏移(*off += 操作位元組數),不可以直接通過"fl->f_pos"來設定

ssize_t (*write) (struct file *, const char __user *buf, size_t len, loff_t *off);
    //使用者程序把資料給驅動,也就是讓驅動存放使用者程序傳進來的資料
    //buf指向使用者程序裡的緩衝區,len表示buf的大小(由使用者呼叫write時傳進來的)
    //off表示fl檔案描述符的操作偏移,返回值為實際給驅動的資料位元組數
    //注意:必須通過off指標來改變檔案描述符的偏移(*off += 操作位元組數),不可以直接通過"fl->f_pos"來設定

long (*unlocked_ioctl) (struct file *fl, unsigned int cmd, unsigned long arg);
    //cmd表示使用者程序呼叫ioctl時的第二個引數,arg表示第三個引數(可選)
    //返回值為0表示ioctl成功,返回負數表示失敗

loff_t (*llseek) (struct file *fl, loff_t offset, int whence);  
    //如:lseek(fd, 54, SEEK_SET)

在驅動裡操作使用者資料緩衝區的函式:

#include <asm/uaccess.h>

extern inline long copy_to_user(void __user *to, const void *from, long n);
    //返回值為還有多少位元組沒有複製成功,正常情況下返回0
    //to指使用者程序的緩衝區,from指驅動裡裝資料的緩衝區,n要複製多少位元組

extern inline long copy_from_user(void *to, const void __user *from, long n);
    //返回值為還有多少位元組沒有複製成功,正常情況下返回0
    //to指驅驅動裡裝資料的緩衝區,from指使用者程序的緩衝區,n指多少位元組

put_user(x, p);
    //x為值,p為地址
    //如果與使用者程序互動的資料是1, 2, 4, 8位元組的話,可用

get_user(x,p);
    //如果從使用者程序獲取1, 2, 4位元組的話,可用

驅動裡動態申請緩衝區的函式:

#include <linux/slab.h>

void *kmalloc(size_t size, gfp_t flags);//申請後要記憶體要清零
void *kzalloc(size_t size, gfp_t flags);//申請出來的記憶體已清零
void kfree(const void *objp);//回收kmalloc/kzalloc的記憶體

void *vmalloc(unsigned long size);//申請大記憶體空間
void vfree(const void *addr);//回收vmalloc的記憶體

//kmalloc申請出來的記憶體是實體地址連續的,vmalloc不一定是連續的
//動態申請記憶體,並清零,size為申請多大(不要超過128K)
//flags為標誌(常為GFP_KERNEL),成功返回地址,失敗返回NULL
//GFP_ATOMIC,使用系統的記憶體緊急池

在驅動裡用資料緩衝區代替文字檔案的資料存取


虛擬檔案系統的實現(test.c):

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

#define MYMA  1234
#define MYMI  5500
#define COUNT 1

dev_t devid; //裝置號id
struct cdev mycdev; //裝置物件
u8 *data; //驅動資料緩衝區
int data_len = 26; //驅動資料緩衝區長度

//當用戶程序讀時,應把驅動裡的緩衝區資料複製給使用者程序
//fl->f_pos表示當前檔案描述符對檔案的偏移, len表示使用者程序想要讀的大小
ssize_t myread(struct file *fl, char __user *buf, size_t len, loff_t *off)
{
    int copy_len, ret;

    if((fl->f_pos + len) > strlen(data)) //如果剩下沒讀的資料長度少於len,則只複製出剩下沒讀部分
        copy_len = strlen(data) - fl->f_pos;
    else
        copy_len = len; //如果剩下的資料長度超出len,則本次複製len位元組

    ret = copy_to_user(buf, data+fl->f_pos, copy_len); //讀取驅動資料緩衝區內容

    //內容複製後,需要改變檔案描述符的位置偏移
    *off += copy_len - ret; //在read/write函式裡必須通過off來改變

    return copy_len - ret;
}

//當用戶程序write時,驅動裡應把使用者程序的資料存放起來
ssize_t mywrite(struct file *fl, const char __user *buf, size_t len, loff_t *off)
{
    int copy_len, ret;

    if((fl->f_pos + len) > data_len) //如果要複製的內容超出資料緩衝區的大小      
        copy_len = data_len - fl->f_pos; //只複製剩下空間大小的內容
    else
        copy_len = len;

    ret = copy_from_user(data+ fl->f_pos, buf, copy_len); //將資料寫入驅動資料緩衝區

    *off += len_copy - ret;

    return len_copy - ret;
}

//當用戶程序lseek(fd, 54, SEEK_SET)時觸發此函式
loff_t myllseek(struct file *fl, loff_t offset, int whence)
{
    //除了read/write函式,可以通過fl->f_pos來改變檔案描述符的偏移
    u8 *tmp;
    int len;

    switch(whence)
    {
        case SEEK_SET:
            if(offset < data_len)
                fl->f_pos = offset;
            else
                return -1;
            break;

        case SEEK_CUR:
            if((fl->f_pos + offset) < data_len)
                fl->f_pos += offset;
            else
                return -1;
            break;

        case SEEK_END:
            if(0 == offset)
                fl->f_pos = data_len - 1; //資料緩衝區的最後位置 
            else if(offset > 0)
            {
                //表示需要把緩衝區變為(原大小+offset)
                len = data_len;
                data_len += offset;

                tmp = kzalloc(data_len, GFP_KERNEL); //分配出新大小的緩衝區
                memcpy(tmp, data, len); //把原緩衝區上的資料複製到新緩衝區
                kfree(data); //回收原空間    
                data = tmp; //取代原緩衝區

                fl->f_pos = data_len - 1;
            }
            else
                return -1;
            break;
    }

    return fl->f_pos;
}

struct file_operations fops = {
    .owner = THIS_MODULE,
    .read = myread,
    .write = mywrite,
    .llseek = myllseek, 
};

static int __init test_init(void)
{
    int ret, i;

    devid = MKDEV(MYMA, MYMI); //生成裝置號
    ret = register_chrdev_region(devid, COUNT, "mydev"); //申請裝置號
    if(ret < 0)
        goto err0;

    cdev_init(&mycdev, &fops); //初始化裝置物件
    mycdev.owner = THIS_MODULE;

    ret = cdev_add(&mycdev, devid, COUNT); //將裝置物件加到核心
    if (ret < 0)
        goto err1;

    data = kzalloc(dlen, GFP_KERNEL); //申請資料緩衝區, 此緩衝區用於與使用者程序資料互動.
    if (NULL == data)
        goto err2;

    //初始化驅動緩衝區內容
    for (i = 0; i < 26; i++)
        data[i] = 'A' + i;

    printk("init cdev success\n");

    return 0;

err2:
    cdev_del(&mycdev); //從核心中移除裝置物件
err1:
    unregister_chrdev_region(devid, COUNT); //回收裝置號
err0:
    return ret;
}

static void __exit test_exit(void)
{
    unregister_chrdev_region(devid, COUNT); //回收裝置號
    cdev_del(&mycdev); //從核心中移除裝置物件
    kfree(data); //釋放核心驅動緩衝區
}

module_init(test_init);
module_exit(test_exit);

MODULE_LICENSE("GPL");

使用者功能實現(app_test.c):

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main(void)
{
    int fd;
    int ret;
    char ch;
    char buf[100];

    fd = open("/dev/mydev", O_RDWR);
    if(fd < 0)
    {
        printf("open file failed\n");
        return -1;
    }

    ret = read(fd, buf, 100); //呼叫核心驅動的myread函式
    if(ret < 0)
    {
        perror("read");
        return -1;
    }
    printf("%s\n", buf);

    lseek(fd, 0, SEEK_SET);//呼叫核心驅動的myllseek函式

    ret = write(fd, "aaaaaaaaaaaat", 13);//呼叫核心驅動的mywrite函式
    if(ret < 0)
    {
        perror("write");
        return -1;
    }

    lseek(fd, 0, SEEK_SET);//呼叫核心驅動的myllseek函式

    while(1)
    {
        ret = read(fd, &ch, 1);//呼叫核心驅動的myread函式
        if(ret < 0)
        {
            perror("read");
            break;
        }
        else if (ret == 0)
            break;
        printf("%c\n", ch);
    }

    close(fd);

    return 0;
}

Makefile檔案:

obj-m += test.o

KSRC := /目錄路徑/orangepi_sdk/source/linux-3.4.112/
export ARCH := arm
export CROSS_COMPILE := arm-linux-gnueabihf-

all : 
    make -C $(KSRC) modules M=`pwd`

.PHONY : clean
clean : 
    make -C $(KSRC) modules clean M=`pwd`

寫好test.c和Makefile檔案後,進行測試步驟:

1.編譯核心驅動test.ko模組:
    make

2.交叉編譯生成使用者功能檔案app:
    arm-linux-gnueabifh-gcc app.c -o app

3.載入驅動模組:
    insmod test.ko

4.執行功能檔案:
    ./app

5.解除安裝驅動模組:
    rmmod test 或 rmmod test.ko

一個驅動支援多個虛擬檔案的實現(test.c):

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

#define MYMA  1234
#define MYMI  7788
#define COUNT 3 //三個裝置號,每個裝置號對應一個裝置檔案和一個數據緩衝區

dev_t devid;
struct cdev mycdev;
u8 *data[COUNT]; //驅動資料緩衝區
int dlen = 1024; //驅動資料緩衝區長度

ssize_t myread(struct file *fl, char __user *buf, size_t len, loff_t *off)
{
    int mi = MINOR(fl->f_path.dentry->d_inode->i_rdev);
    int len_copy, ret, n;

    n = mi - MYMI;
    if (n >= COUNT)
        return -ENODEV;

    if ((fl->f_pos + len) > strlen(data[n])) 
            len_copy = strlen(data[n]) - fl->f_pos;
    else
            len_copy = len; 

    ret = copy_to_user(buf, data[n]+fl->f_pos, len_copy);

    *off += len_copy - ret; 
    return len_copy - ret;
}

ssize_t mywrite(struct file *fl, const char __user *buf, size_t len, loff_t *off)
{
    int mi = MINOR(fl->f_path.dentry->d_inode->i_rdev);
    int len_copy, ret, n;

    n = mi - MYMI;
    if (n >= COUNT)
        return -ENODEV;
    if ((fl->f_pos + len) > dlen) 
        len_copy = dlen - fl->f_pos; 
    else
        len_copy = len;

    ret = copy_from_user(data[n] + fl->f_pos, buf, len_copy);

    *off += len_copy - ret;

    return len_copy - ret;
}

struct file_operations fops = {
    .owner = THIS_MODULE,
    .read = myread,
    .write = mywrite,
};

static int __init test_init(void)
{
    int ret, i, j;

    devid = MKDEV(MYMA, MYMI);
    ret = register_chrdev_region(devid, COUNT, "mydev");
    if (ret < 0)
        goto err0;

    cdev_init(&mycdev, &fops);
    mycdev.owner = THIS_MODULE;

    ret = cdev_add(&mycdev, devid, COUNT);
    if (ret < 0)
        goto err1;

    for (i = 0; i < COUNT; i++)
    {
        data[i] = kzalloc(dlen, GFP_KERNEL); 
        for (j = 0; j < 26; j++)
            data[i][j] = 'A' + j;
    }

    return 0;

err1:
    unregister_chrdev_region(devid, COUNT);
err0:
    return ret;
}

static void __exit test_exit(void)
{
    int i;

    unregister_chrdev_region(devid, COUNT);
    cdev_del(&mycdev);

    for (i = 0; i < COUNT; i++)
        kfree(data[i]);
}

module_init(test_init);
module_exit(test_exit);

MODULE_LICENSE("GPL");