嵌入式Linux 用户层和内核层交互

  • Post author:
  • Post category:linux


要明确一个概念, Linux 一切皆文件! 驱动文件最终通过与文件操作相关的系统调用或者 C 库函数(本质也是系统调用)被访问, 而设备驱动的结构最终也是为了迎合提供给应用程序的 API。(在 Windows编程领域, 习惯称操作系统的接口为 API) 。 先来了解一下基本的概念。


概念 1 设备节点


在 Linux 中, 所有设备都以文件的形式存放在/dev 目录下, 都是通过文件的方式进行访问, 设备节点是Linux 内核对设备的抽象, 一个设备节点就是一个文件。 应用程序通过一组标准化的调用执行访问设备, 这些调用独立于任何特定的驱动程序。 而驱动程序负责将这些标准调用映射到实际硬件的特有操作。

设备节点, 驱动, 硬件设备是怎样关联到一起的呢? 这是通过设备号实现的, 包括主设备号和次设备号。 当我们创建一个设备节点时需要指定主设备号和次设备号。 应用程序通过名称访问设备, 而设备号指定了对应的驱动程序和对应的设备。 主设备号标识设备对应的驱动程序, 次设备号由内核使用, 用于确定设备节点所指设备。

主设备号: 驱动程序在初始化时, 会注册它的驱动及对应主设备号到系统中, 这样当应用程序访问设备节点时, 系统就知道它所访问的驱动程序了。 你可以通过/proc/devices 文件来查看系统设备的主设备号。

次设备号: 驱动程序遍历设备时, 每发现一个它能驱动的设备, 就创建一个设备对象, 并为其分配一个次设备号以区分不同的设备。 这样当应用程序访问设备节点时驱动程序就可以根据次设备号知道它说访问的设备了。

设备节点(设备文件) : Linux 中设备节点是通过“mknod”命令来创建的。 一个设备节点其实就是一个文件,Linux 中称为设备文件。 有一点必要说明的是, 在 Linux 中, 所有的设备访问都是通过文件的方式, 一般的数据文件称为普通文件, 设备节点称为设备文件。 设备节点就是连接上层应用和底层驱动的桥梁, 如下图所示:

设备驱动: 设备驱动程序(device driver) , 简称驱动程序(driver) , 是一个允许高级(High level)计算机软件(computer software) 与硬件(hardware) 交互的程序, 这种程序建立了一个硬件与硬件, 或硬件与软件沟通的界面, 经由主板上的总线(bus) 或其它沟通子系统(subsystem) 与硬件形成连接的机制, 这样的机制使得硬件设备(device) 上的数据交换成为可能。 想象平时我们说的写驱动, 例如点 led 灯的驱动, 就是简单的 io 操作。

文件对应的操作有打开, 关闭, 读写, 那么设备节点也可以看成一个文件, 那么设备节点对应的操作有打开, 关闭, 读写。 如果我在应用层使用系统 IO(系统调用) 对设备节点进行打开, 关闭, 读写等操作会发生什么呢?

file_operations 结构体是访问驱动的函数, 它的里面的每个结构体成员都对应一个调用, 这个结构体里面有很多的成员变量, 并且结构体中的成员函数是字符设备驱动程序设计的主体内容, 这些函数实际会在应用程序进行 Linux 的 open() 、 write() 、 read() 、 close() 等系统调用时最终被内核调用。 file_operations文件操作集在定义在 include/linux/fs.h 下面, 如下图所示。

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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    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 (*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 (*aio_fsync) (struct kiocb *, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long,unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **);
    long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);
    int (*show_fdinfo)(struct seq_file *m, struct file *f);
};

下面对 file_operations 结构体中的主要成员进行分析。

函数 功能
llseek() 函数 修改一个文件的当前读写位置, 并将新位置返回, 在出错时, 这个函数返回一个负值。
read() 函数 用来从设备中读取数据, 成功时函数返回读取的字节数, 出错时返回一个负值。 它与

用户空间应用程序中的 ssize_t read( int fd, void*buf, size_t count) 和 size_t fread

(void*ptr, size_t size, size_t nmemb, FILE*stream) 对应。
write() 函数 向设备发送数据, 成功时该函数返回写入的字节数。 如果此函数未被实现, 当用户进

行 write() 系统调用时, 将得到-EINVAL 返回值。 它与用户空间应用程序中的 ssize_t

write(int fd, const void*buf, size_t count) 和 size_t fwrite(const void*ptr, size_t size,

size_t nmemb, FILE*stream) 对应。
read( ) 和 write

()
如果返回 0, 则暗示 end-of-file(EOF) 。
unlocked_ioctl() 提供设备相关控制命令的实现(既不是读操作, 也不是写操作) , 当调用成功时, 返

回给调用程序一个非负值。 它与用户空间应用程序调用的 int fcntl( int fd, int

cmd, …/*arg*/) 和 int ioctl(int d, int request, …) 对应。
mmap( ) 函数 将设备内存映射到进程的虚拟地址空间中, 如果设备驱动未实现此函数, 用户进行

mmap( ) 系统调用时将获得-ENODEV 返回值。 这个函数对于帧缓冲等设备特别有意

义, 帧缓冲被映射到用户空间后, 应用程序可以直接访问它而无须在内核和应用间进

行内存复制。 它与用户空间应用程序中的

void*mmap( void*addr, size_t length, int prot, int flags, int fd, off_t offset) 函数对

应。
poll( ) 函数 一般用于询问设备是否可被非阻塞地立即读写。 当询问的条件未触发时, 用户空间进

行 select( ) 和 poll( ) 系统调用将引起进程的阻塞。
aio_read ( ) 和

aio_write( ) 函数
分别对与文件描述符对应的设备进行异步读、 写操作。 设备实现这

两个函数后, 用户空间可以对该设备文件描述符执行 SYS_io_setup、 SYS_io_submit、

SYS_io_getevents、 SYS_io_destroy 等系统调用进行读写。
open( ) 函数 当用户空间调用 Linux API 函数 open( ) 打开设备文件时, 设备驱动的 open( ) 函数

最终被调用。 驱动程序可以不实现这个函数, 在这种情况下, 设备的打开操作永远成

功。 与 open( ) 函数对应的是 release( ) 函数。

实际情况下我们是用不了这么多调用的。 常用的调用如下所示:

当我们在应用层 read 设备节点的时候, 就会触发我们驱动里面 read 这个函数。

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

当我们在应用层 write 设备节点的时候, 就会触发我们驱动里面 write 这个函数。

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

当我们在应用层 poll/select 的时候, 就会触发我们驱动里面 poll 这个函数。

unsigned int (*poll) (struct file *, struct poll_table_struct *);

当我们在应用层 ioctl 的时候, 就会触发我们驱动里面 ioctl 这个函数。

long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);

当我们在应用层 open 的时候, 就会触发我们驱动里面 open 这个函数。

int (*open) (struct inode *, struct file *);

当我们在应用层 close 的时候, 就会触发我们驱动里面 close 这个函数。

int (*release) (struct inode *, struct file *);


编写驱动程序和应用程序


填充 file_operation 结构体

//文件操作集
struct file_operations misc_fops={
    .owner = THIS_MODULE,
    .open = misc_open,
    .release = misc_release,
    .read = misc_read,
    .write = misc_write,
};

填充接口函数

ssize_t misc_read (struct file *file, char __user *ubuf, size_t size, loff_t *loff_t)
{
    printk("misc_read\n ");
    return 0;
} 

ssize_t misc_write (struct file *file, const char __user *ubuf, size_t size, loff_t *loff_t)
{
    printk("misc_write\n ");
    return 0;
} 

int misc_release(struct inode *inode,struct file *file)
{
    printk("hello misc_relaease bye bye \n ");
    return 0;
} 

int misc_open(struct inode *inode,struct file *file)
{
    printk("hello misc_open\n ");
    return 0;
}

完整驱动代码如下:

/** 
*@Descripttion: 在上一章节实现了最简单杂项设备的编写, 本代码再其基础上验证内核层与应用层数据交互
* @version:
* @Author: Apple
* @Date: 2021-02-23 10:35:15
* @LastEditors: sueRimn
* @LastEditTime: 2021-02-23 10:50:47
*/

#include <linux/init.h> //初始化头文件
#include <linux/module.h> //最基本的文件, 支持动态添加和卸载模块。
#include <linux/miscdevice.h>/*注册杂项设备头文件*/
/**
* @name: misc_read
* @test: 从设备中读取数据, 当用户层调用函数 read 时, 对应的, 内核驱动就会调用这个函数。
* @msg:
* @param {structfile} *file file 结构体
* @param {char__user} *ubuf 这是对应用户层的 read 函数的第二个参数 void *buf
* @param {size_t} size 对应应用层的 read 函数的第三个参数
* @param {loff_t} *loff_t 这是用于存放文件的偏移量的, 回想一下系统编程时, 读写文件的操作都会使偏移量往后移。
* @return {*} 当返回正数时, 内核会把值传给应用程序的返回值。 一般的, 调用成功会返回成功读取的字节数。 如果返回负数, 内核就会认为这是错误, 应用程序返回-1
*/

ssize_t misc_read (struct file *file, char __user *ubuf, size_t size, loff_t *loff_t)
{
    printk("misc_read\n ");
    return 0;
} 
/**
* @name: misc_write
* @test: 往设备写入数据, 当用户层调用函数 write 时, 对应的, 内核驱动就会调用这个函数。
* @msg:
* @param {structfile} * filefile 结构体
* @param {constchar__user} *ubuf 这是对应用户层的 write 函数的第二个参数 const void *buf
* @param {size_t} size 对应用户层的 write 函数的第三个参数 count。
* @param {loff_t} *loff_t 这是用于存放文件的偏移量的, 回想一下系统编程时, 读写文件的操作都会使偏移量往后移。
* @return {*} 当返回正数时, 内核会把值传给应用程序的返回值。 一般的, 调用成功会返回成功读取的字节数。 如果返回负数, 内核就会认为这是错误, 应用程序返回-1。
*/

ssize_t misc_write (struct file *file, const char __user *ubuf, size_t size, loff_t *loff_t)
{
    printk("misc_write\n ");
    return 0;
} 
/**
* @name: misc_release
* @test: 当设备文件被关闭时内核会调用这个操作, 当然这也可以不实现, 函数默认为 NULL。 关闭设备永远成功。
* @msg:
* @param {structinode} *inode 设备节点
* @param {structfile} *file filefile 结构体
* @return {0}
*/
int misc_release(struct inode *inode,struct file *file)
{ 
    printk("hello misc_relaease bye bye \n ");
    return 0;
}
 
/**
* @name: misc_open
* @test: 在操作设备前必须先调用 open 函数打开文件, 可以干一些需要的初始化操作。
* @msg:
* @param {structinode} *inode 设备节点
* @param {structfile} *file filefile 结构体
* @return {0}
*/
int misc_open(struct inode *inode,struct file *file)
{
    printk("hello misc_open\n ");
    return 0;
} 

//文件操作集
struct file_operations misc_fops={
    .owner = THIS_MODULE,
    .open = misc_open,
    .release = misc_release,
    .read = misc_read,
    .write = misc_write,
};

//miscdevice 结构体
struct miscdevice misc_dev = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "hello_misc",
    .fops = &misc_fops,
};

static int misc_init(void)
{
    int ret;
    ret = misc_register(&misc_dev);//注册杂项设备
    if(ret<0)
    {
        printk("misc registe is error \n");
    }
    printk("misc registe is succeed \n");
    return 0;
} 

static void misc_exit(void)
{
    misc_deregister(&misc_dev); //卸载杂项设备
    printk(" misc gooodbye! \n");
} 
module_init(misc_init);
module_exit(misc_exit);
MODULE_LICENSE("GPL");

编写应用程序 app.c,完整代码如下图所示:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
    int fd;//定义一个句柄
    char buf[64] = {0};
    fd = open("/dev/hello_misc",O_RDWR);//打开设备节点
    if(fd < 0)
    {
    perror("open error \n");
    return fd;
    } 
    //read(fd,buf,sizeof(buf));
    write(fd,buf,sizeof(buf));
    close(fd);
    return 0;
}

将 app.c 文件拷贝到 Ubuntu 的/home/topeet/driver/imx6ull/file_operation 目录下, 输入以下命令编译 app.c, 编译生成的 APP 在其他开发板上也是可以运行的。

arm-none-linux-gnueabi-gcc app.c -o app -static

编译完成如下图所示:


编译驱动及运行测试


Makefile 文件拷贝到 file_operation.c 同级目录下, 修改 Makefile 为:

obj-m += file_operation.o #先写生成的中间文件的名字是什么, -m 的意思是把我们的驱动编译成模块
KDIR:=/home/topeet/driver/imx6ull/linux-imx-rel_imx_4.1.15_2.1.0_ga/
PWD?=$(shell pwd) #获取当前目录的变量
all:
    make -C $(KDIR) M=$(PWD) modules #make 会进入内核源码的路径, 然后把当前路径下的代码编译成
模块

输入以下命令, 我们进入到共享目录, 加载驱动模块如图所示:

cd /mnt/nfs/imx6ull/file_operation/

insmod file_operation.ko

ls /dev/hello_misc

将 编译好的 APP 可执行文件拷贝到 Ubuntu的/home/topeet/driver/imx6ull/file_operation目录下, 运行如下图所示:


应用层和内核层传递数据


应用层和内核层是不能直接进行数据传输的。 我们要想进行数据传输, 要借助下面的这两个函数。

static inline long copy_from_user(void *to, const void __user * from, unsigned long n)

static inline long copy_to_user(void __user *to, const void *from, unsigned long n)

用户空间–>内核空间, 如下图所示:

函数 copy_from_user(void *to, const void __user *from, unsigned long n)
参数 to 目标地址(内核空间)
参数 from 源地址(用户空间)
参数 n 将要拷贝数据的字节数
返回值 成功返回 0, 失败返回没有拷贝成功的数据字节数
功能 将用户空间数据拷贝到内核空间

内核空间–>用户空间, 如下图所示:

函数 copy_to_user(void __user *to, const void *from, unsigned long n)
参数 to 目标地址(用户空间)
参数 from 源地址(内核空间)
参数 n 将要拷贝数据的字节数
返回值 成功返回 0, 失败返回没有拷贝成功的数据字节数
功能 将内核空间数据拷贝到用户空间


应用层从内核层读数据

修改上面编写的驱动程序, 要从内核层读数据, 完整代码如下所示:

#include <linux/init.h> //初始化头文件
#include <linux/module.h> //最基本的文件, 支持动态添加和卸载模块。
#include <linux/miscdevice.h>/*注册杂项设备头文件*/
#include <linux/uaccess.h>
#include <linux/fs.h>
/**
* @name: misc_read
* @test: 从设备中读取数据, 当用户层调用函数 read 时, 对应的, 内核驱动就会调用这个函数。
* @msg:
* @param {structfile} *file file 结构体
* @param {char__user} *ubuf 这是对应用户层的 read 函数的第二个参数 void *buf
* @param {size_t} size 对应应用层的 read 函数的第三个参数
* @param {loff_t} *loff_t 这是用于存放文件的偏移量的, 回想一下系统编程时, 读写文件的操作都会使偏移量往后移。
* @return {*} 当返回正数时, 内核会把值传给应用程序的返回值。 一般的, 调用成功会返回成功读取的字节数。 如果返回负数, 内核就会认为这是错误, 应用程序返回-1
*/
ssize_t misc_read (struct file *file, char __user *ubuf, size_t size, loff_t *loff_t) 
{
    char kbuf[] = "hehe";
    if(copy_to_user(ubuf,kbuf,strlen(kbuf))!=0)
    {
        printk("copy_to_user error\n ");
        return -1;
    } 
    printk("misc_read\n ");
    return 0;
}
/**
* @name: misc_write
* @test: 往设备写入数据, 当用户层调用函数 write 时, 对应的, 内核驱动就会调用这个函数。
* @msg:
* @param {structfile} * filefile 结构体
* @param {constchar__user} *ubuf 这是对应用户层的 write 函数的第二个参数const void *buf
* @param {size_t} size 对应用户层的 write 函数的第三个参数 count。
* @param {loff_t} *loff_t 这是用于存放文件的偏移量的, 回想一下系统编程时, 读写文件的操作都会使偏移量往后移。
* @return {*} 当返回正数时, 内核会把值传给应用程序的返回值。 一般的, 调用成功会返回成功读取的字节数。 如果返回负数, 内核就会认为这是错误, 应用程序返回-1。
*/
ssize_t misc_write (struct file *file, const char __user *ubuf, size_t size, loff_t *loff_t)
{
    printk("misc_write\n ");
    return 0;
} 
/**
* @name: misc_release
* @test: 当设备文件被关闭时内核会调用这个操作, 当然这也可以不实现, 函数默认为 NULL。 关闭设备永远成功。
* @msg:
* @param {structinode} *inode 设备节点
* @param {structfile} *file filefile 结构体
* @return {0}
*/
int misc_release(struct inode *inode,struct file *file)
{ 
    printk("hello misc_relaease bye bye \n ");
    return 0;
} 
/**
* @name: misc_open
* @test: 在操作设备前必须先调用 open 函数打开文件, 可以干一些需要的初始化操作。
* @msg:
* @param {structinode} *inode 设备节点
* @param {structfile} *file filefile 结构体
* @return {0}
*/
int misc_open(struct inode *inode,struct file *file)
{
    printk("hello misc_open\n ");
    return 0;
} 

//文件操作集
struct file_operations misc_fops={
    .owner = THIS_MODULE,
    .open = misc_open,
    .release = misc_release,
    .read = misc_read,
    .write = misc_write,
};

//miscdevice 结构体
struct miscdevice misc_dev = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "hello_misc",
    .fops = &misc_fops,
};

static int misc_init(void)
{
    int ret;
    ret = misc_register(&misc_dev);//注册杂项设备
    if(ret<0)
    {
    printk("misc registe is error \n");
    } 
    printk("misc registe is succeed \n");
    return 0;
} 

static void misc_exit(void)
{
    misc_deregister(&misc_dev);//卸载杂项设备
    printk(" misc gooodbye! \n");
} 

module_init(misc_init);
module_exit(misc_exit);
MODULE_LICENSE("GPL");

应用程序修改如下:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
    int fd;
    char buf[64] = {0};
    fd = open("/dev/hello_misc",O_RDWR);//打开设备节点
    if(fd < 0)
    {
        perror("open error \n");
        return fd;
    } 
    read(fd,buf,sizeof(buf));//读内核层数据
    printf("buf is %s\n",buf);
    close(fd);
    return 0;
}

再次编译驱动程序和应用程序, 加载驱动模块和运行应用程序如下图所示:


应用层向内核层写数据


修改上面编写的驱动函数, 要从应用层读数据, 完整代码如下所示:

#include <linux/init.h> //初始化头文件
#include <linux/module.h> //最基本的文件, 支持动态添加和卸载模块。
#include <linux/miscdevice.h>//注册杂项设备头文件
#include <linux/uaccess.h>
#include <linux/fs.h>
/**
* @name: misc_read
* @test: 从设备中读取数据, 当用户层调用函数 read 时, 对应的, 内核驱动就会调用这个函数。
* @msg:
* @param {structfile} *file file 结构体
* @param {char__user} *ubuf 这是对应用户层的 read 函数的第二个参数 void *buf
* @param {size_t} size 对应应用层的 read 函数的第三个参数
* @param {loff_t} *loff_t 这是用于存放文件的偏移量的, 回想一下系统编程时, 读写文件的操作都会使偏移量往后移。
* @return {*} 当返回正数时, 内核会把值传给应用程序的返回值。 一般的, 调用成功会返回成功读取的字节数。 如果返回负数, 内核就会认为这是错误, 应用程序返回-1
*/
ssize_t misc_read (struct file *file, char __user *ubuf, size_t size, loff_t *loff_t)
{
    char kbuf[] = "hehe";
    if(copy_to_user(ubuf,kbuf,strlen(kbuf))!=0)
    {
        printk("copy_to_user error\n ");
        return -1;
    } 
    printk("misc_read\n ");
    return 0;
} 
/**
* @name: misc_write
* @test: 往设备写入数据, 当用户层调用函数 write 时, 对应的, 内核驱动就会调用这个函数。
* @msg:
* @param {structfile} * filefile 结构体
* @param {constchar__user} *ubuf 这是对应用户层的 write 函数的第二个参数const void *buf
* @param {size_t} size 对应用户层的 write 函数的第三个参数 count。
* @param {loff_t} *loff_t 这是用于存放文件的偏移量的, 回想一下系统编程时, 读写文件的操作都会使偏移量往后移。
* @return {*} 当返回正数时, 内核会把值传给应用程序的返回值。 一般的, 调用成功会返回成功读取的字节数。 如果返回负数, 内核就会认为这是错误, 应用程序返回-1。
*/
ssize_t misc_write (struct file *file, const char __user *ubuf, size_t size)
{
    char kbuf[64]={0};
    if(copy_from_user(kbuf,ubuf,size)!=0)
    { 
        printk("copy_from_user error\n ");
        return -1;
    }
    printk("kbuf is %s\n ",kbuf);
    return 0;
}
 
/**
* @name: misc_release
* @test: 当设备文件被关闭时内核会调用这个操作, 当然这也可以不实现, 函数默认为 NULL。 关闭设备永远成功。
* @msg:
* @param {structinode} *inode 设备节点
* @param {structfile} *file filefile 结构体
* @return {0}
*/
int misc_release(struct inode *inode,struct file *file)
{
    printk("hello misc_relaease bye bye \n ");
    return 0;
} 
/**
* @name: misc_open
* @test: 在操作设备前必须先调用 open 函数打开文件, 可以干一些需要的初始化操作。
* @msg:
* @param {structinode} *inode 设备节点
* @param {structfile} *file filefile 结构体
* @return {0}
*/
int misc_open(struct inode *inode,struct file *file)
{
    printk("hello misc_open\n ");
    return 0;
} 

//文件操作集
struct file_operations misc_fops={
    .owner = THIS_MODULE,
    .open = misc_open,
    .release = misc_release,
    .read = misc_read,
    .write = misc_write,
};

//miscdevice 结构体
struct miscdevice misc_dev = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "hello_misc",
    .fops = &misc_fops,
};

static int misc_init(void)
{
   int ret;
   ret = misc_register(&misc_dev);//注册杂项设备
   if(ret<0)
   {
      printk("misc registe is error \n");
   } 
    printk("misc registe is succeed \n");
    return 0;
} 

static void misc_exit(void)
{
    misc_deregister(&misc_dev);//卸载杂项设备
    printk(" misc gooodbye! \n");
} 

module_init(misc_init);
module_exit(misc_exit);
MODULE_LICENSE("GPL");

应用程序修改如下:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
    int fd;
    char buf[64] = "12345";
    fd = open("/dev/hello_misc",O_RDWR);//打开设备节点
    if(fd < 0)
    {
        perror("open error \n");
       return fd;
    } 
    write(fd,buf,sizeof(buf)); //向内核层写数据
    close(fd);
    return 0;
}

再次编译驱动程序和应用程序, 加载驱动模块和运行应用程序如下图所示:



版权声明:本文为yangxueyangxue原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。