编写最简单的字符设备驱动

编写最简单的字符设备驱动

  • 1 编写驱动代码
  • 2 编写makefile
  • 3 编译和加载驱动
  • 4 编写应用程序测试驱动

参考文章:
linux驱动开发第1讲:带你编写一个最简单的字符设备驱动

linux驱动开发第2讲:应用层的write如何调用到驱动中的write

1 编写驱动代码

驱动代码chardev.c如下:

#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/sched.h>
#include <linux/slab.h>#define BUFFER_MAX    (10)
#define OK            (0)
#define ERROR         (-1)struct cdev *gDev;
struct file_operations *gFile;
dev_t  devNum;
unsigned int subDevNum = 1;
int reg_major  =  232;    
int reg_minor =   0;
char *buffer;
int flag = 0;
int hello_open(struct inode *p, struct file *f)
{printk(KERN_EMERG"hello_open\r\n");return 0;
}ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l)
{printk(KERN_EMERG"hello_write\r\n");return 0;
}
ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l)
{printk(KERN_EMERG"hello_read\r\n");      return 0;
}
int hello_init(void)
{devNum = MKDEV(reg_major, reg_minor);   /* 获取设备号 */if(OK == register_chrdev_region(devNum, subDevNum, "helloworld")){printk(KERN_EMERG"register_chrdev_region ok \n"); }else {printk(KERN_EMERG"register_chrdev_region error n");return ERROR;}printk(KERN_EMERG" hello driver init \n");gDev = kzalloc(sizeof(struct cdev), GFP_KERNEL);gFile = kzalloc(sizeof(struct file_operations), GFP_KERNEL);gFile->open = hello_open;gFile->read = hello_read;gFile->write = hello_write;gFile->owner = THIS_MODULE;cdev_init(gDev, gFile);cdev_add(gDev, devNum, 3);return 0;
}void __exit hello_exit(void)
{cdev_del(gDev);unregister_chrdev_region(devNum, subDevNum);return;
}
module_init(hello_init);    /* 驱动入口 */
module_exit(hello_exit);    /* 驱动出口 */
MODULE_LICENSE("GPL");

hello_init是驱动的入口点,它通过moduel_init注册到系统中,在驱动被装载时调用,module_init()注册的函数原型必须是:

int my_init(void);

所以hello_init的返回值是int类型,没有参数。

hello_exit是驱动的出口函数,有module_exit()注册到系统中,在驱动被卸载时调用,module_exit()注册的函数原型必须是:

void my_exit(void);

所以hello_exit()没有返回,也没有参数。

内核提供打印函数printk(),和C库提供的printf()函数功能几乎相同。

printk(日志级别 "消息文本")

日志级别有8 ,定义在linyx/kernel中如图:
在这里插入图片描述

2 编写makefile

Makefile内容

obj-m := chardev.oKERNELDIR := /lib/modules/$(shell uname -r)/buildall default:modules
install:modules_installmodules modules_install help clean:$(MAKE) -C $(KERNELDIR) M=$(shell pwd) $@
  • obj-m := chardev.o:obj-m列出要构建的模块,对于每一个<filename>.o,进行系统构建时会查找<filename>.c 。obj-m用于构建模块,把模块放在内核源码树外维护,obj-y用于构建内核对象,把模块加入到内核源码树中。
  • KERNELDIR := /lib/modules/$(shell uname -r)/build:KERNELDIR 是欲构建的内核源码位置。如果已经从源代码构建了内核,则应该把这个变量设置为内核构建源代码目录的绝对路径。-C 要求make在读取makefile或执行其他任何操作之前先更改到指定的目录。
  • M=$(shell pwd):内核makefile使用这个变量来定位要构建的外部模块的目录。.c文件应该放在该目录下。
  • all default:modules:此行指示make执行modules目标,在构建用户应用程序时,无论是all还是default都是传统目标,换句话说,make default、make all或者简单的make命令都被翻译为make modules来执行。
  • $(MAKE) -C $(KERNELDIR) M=$(shell pwd) $@:为上面列举的每个目标所执行的规则,$@被替换为引起规则运行的目标名称。换句话说,如果调用make modules,则
    $@被替换成modules,规则将被替换为$(MAKE) -C $(KERNELDIR) M=$(shell pwd) modules

linux应用层程序在编译的时候,需要链接c库和glibc库,那驱动需不需要呢?

驱动也需要,但是驱动不能链接和使用应用层层的任何lib库,驱动需要引用内核的头文件和函数。所以,编译的时候需要指定内核源码的位置,KERNERLDIR就是指定内核源码的位置。

3 编译和加载驱动

在构建外部模块(makefile文件里面使用 obj-m)之前,需要有一个完整的、预编译的内核源代码树,内核源码树版本必须与将加载和使用模块的内核相同。有两种方法可以获得预构建的内核版本。

  • 自己下载源代码,然后构建内核
  • 从发行版库安装linux-headers- *包
sudo apt-get update
sudo apt-get install linux-headers-$(uname -r)

这将只安装头文件,而不是整个源代码树,然后头文件将被安装在/usr/src/linux-headers-$(uname -r)下。有一个符号链接/lib/modules/$(uname -r)/build,指向前面安装的头文件,是在makefile中指定位内核目录的路径。这就是需要为预构建的内核所做的一切。

在驱动目录下,指向make进行编译:
在这里插入图片描述

编译出来的驱动文件为chardev.ko。
在加载驱动之前,我们可以将日志清理一下,方便我们查看驱动产生的消息,使用命令:

dmesg -c

接下来我们把这个驱动加载到内核,使用命令:

sudo insmod chardev.ko

加载的时候就会执行hello_init函数,接着使用命令查看printk输出的消息:
在这里插入图片描述
使用lsmod命令查看系统加载的驱动,可以发现chardev已经加载了
在这里插入图片描述
卸载驱动使用命令:

sudo rmmod chardev.ko

卸载的时候会执行hello_exit()函数

4 编写应用程序测试驱动

本节来看驱动的测试。

我们需要编写一个应用层的程序来对驱动进行测试:(test.c)

#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/select.h>#define DATA_NUM    (64)
int main(int argc, char *argv[])
{int fd, i;int r_len, w_len;fd_set fdset;char buf[DATA_NUM]="hello world";memset(buf,0,DATA_NUM);fd = open("/dev/hello", O_RDWR);printf("%d\r\n",fd);if(-1 == fd) {perror("open file error\r\n");return -1;}	else {printf("open successe\r\n");}w_len = write(fd,buf, DATA_NUM);r_len = read(fd, buf, DATA_NUM);printf("%d %d\r\n", w_len, r_len);printf("%s\r\n",buf);return 0;
}

编译并执行,发现错误,找不到设备文件:

在这里插入图片描述
这是因为还没有创建驱动的设备文件,我们为驱动手动创建设备文件

 sudo mknod /dev/hello c 232 0

注意,这里的232和0要跟驱动文件chardev.c里定义的主次设备号对应起来。

我们再次执行:sudo ./test
在这里插入图片描述
发现成功了,我们执行dmesg查看驱动输出,发现驱动里的hell_open, hello_write, hello_read被依次调用了。
在这里插入图片描述
这就是一个完整的、最简单的驱动的开发和测试的流程。

对于应用程序中的write函数如何调用到驱动力的write函数,先上一张图简单说明下调用流程。
请添加图片描述

用户空间的程序无法直接执行内核代码,它们不能直接调用内核空间中的函数,所以应用程序会以某种方式通知系统,告诉内核自己需要执行一个系统调用,系统系统切换到内核态,通知内核的机制是靠软中断实现的:通过一个异常来促使系统切换到内核态去执行异常处理程序,此时异常处理程序就是系统调用程序,叫system_call(),system_call()根据系统调用号去执行相关的系统调用。

整个流程,上图表现的已经非常明显,但是问题也是有的,操作系统中的系统调用最终是如何知道应该调用哪个驱动里的write函数呢?

如果我们没有记错,在驱动文件里,有定义主次设备号:

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_t的结构体如下:

struct cdev   
{  struct kobject kobj;  struct module *owner; //所属模块  const struct file_operations *ops; //文件操作结构  struct list_head list;  dev_t dev; //设备号,int 类型,高12位为主设备号,低20位为次设备号  unsigned int count;  
};  

cdev_init(gDev, gFile); 建立了gDev和gFile的逻辑关系,初始化gDev结构体中的ops。
cdev_add(gDev, devNum, 3); 建立了gDev和devNum的逻辑关系; cdev_add 用于向Linux内核系统中添加一个新的cdev结构体变量所描述的字符设备,并且使这个设备立即可用。gDev是被添加入Linux内核系统的字符设备,devNum代表设备的设备号,其中包括主设备号和次设备号,3代表想注册设备的设备号的范围,用于给struct cdev中的字段count赋值。

其实你翻开代码看细节会发现,以上两句代码其实建立了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之后,就已经建立了这个文件和驱动里的 struct file 的对应关系,也就建立了这个文件和驱动里的struct file_operations的对应关系。

好,了解以上的背景之后,我们来看看代码。

我们从内核里write系统调用的实现部分开始阅读:

相关的代码在:fs/read_write.c

ssize_t ksys_write(unsigned int fd, const char __user *buf, size_t count)
{struct fd f = fdget_pos(fd);ssize_t ret = -EBADF;if (f.file) {loff_t pos = file_pos_read(f.file);ret = vfs_write(f.file, buf, count, &pos);if (ret >= 0)file_pos_write(f.file, pos);fdput_pos(f);}return ret;
}

关键代码在vfs_write。所以,我们继续跟进入:

ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
{ssize_t ret;if (!(file->f_mode & FMODE_WRITE))return -EBADF;if (!(file->f_mode & FMODE_CAN_WRITE))return -EINVAL;if (unlikely(!access_ok(buf, count)))return -EFAULT;ret = rw_verify_area(WRITE, file, pos, count);if (!ret) {if (count > MAX_RW_COUNT)count =  MAX_RW_COUNT;file_start_write(file);ret = __vfs_write(file, buf, count, pos);if (ret > 0) {fsnotify_modify(file);add_wchar(current, ret);}inc_syscw(current);file_end_write(file);}return ret;
}

继续跟入__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);elsereturn -EINVAL;
}

关键代码在这里:

if (file->f_op->write)return file->f_op->write(file, p, count, pos);

上面提到建立了/dev/hello和file_operations的关系。所以这里其实就是判断chardev驱动里有没有定义write函数,如果有,那就调用驱动里的write函数。

应用程序的write函数去调用C库里面的write函数,C库里面的write函数会产生一个异常,进入内核空间,调用系统调用函数system_call()。system_call()根据系统调用号去调用sys_wtite函数,sys_write函数根据应用程序传来的fd找到file operation,也就是驱动定义的文件(gDev)可以执行那些操作,找到file operation后,调用file operation里面的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_write\r\n");return 0;
}
ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l)
{printk(KERN_EMERG"hello_read\r\n");      return 0;
}

如果你想让测试程序里的write和read返回非零值,只要把驱动里的return 0,改为任意值就好了,大家可以自己测试一下。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/news/379581.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java ObjectStreamField toString()方法与示例

ObjectStreamField类toString()方法 (ObjectStreamField Class toString() method) toString() method is available in java.io package. toString()方法在java.io包中可用。 toString() method is used to return a string that defines this field. toString()方法用于返回定…

linux内核文件描述符fd、文件索引节点inode、文件对象file关系

文件描述符fd、文件索引节点inode、文件对象file关系1 VFS对象1.1 超级块对象1.2 索引节点对象1.3 文件对象1.4 进程描述符1.5 files_struct2 如何根据文件描述符fd找到文件&#xff1f;1 VFS对象 在说fd、inode和file关系之前&#xff0c;我们先了解VFS的几个概念。分别是进程…

sql2005 获取表字段信息和视图字段信息

获取表字段名,和字段说明SELECT[Table Name]OBJECT_NAME(c.object_id), [ColumnName]c.name, [Description]ex.value FROMsys.columns c LEFTOUTERJOINsys.exte…

解析css之position

CSS的很多其他属性大多容易理解&#xff0c;比如字体&#xff0c;文本&#xff0c;背景等。有些CSS书籍也会对这些简单的属性进行大张旗鼓的介绍&#xff0c;而偏偏忽略了对一些难缠的属性讲解&#xff0c;有避重就轻的嫌疑。CSS中主要难以理解的属性包括盒型结构&#xff0c;以…

Java ObjectInputStream readLong()方法(带示例)

ObjectInputStream类readLong()方法 (ObjectInputStream Class readLong() method) readLong() method is available in java.io package. readLong()方法在java.io包中可用。 readLong() method is used to read 8 bytes (i.e. 64 bit) of long value from this ObjectInputSt…

交换瓶子(蓝桥杯)

有N个瓶子&#xff0c;编号 1 ~ N&#xff0c;放在架子上。 比如有5个瓶子&#xff1a; 2 1 3 5 4 要求每次拿起2个瓶子&#xff0c;交换它们的位置。 经过若干次后&#xff0c;使得瓶子的序号为&#xff1a; 1 2 3 4 5 对于这么简单的情况&#xff0c;显然&#xff0c;至少…

Linux设备驱动开发---字符设备驱动程序

字符设备驱动程序1 主设备和次设备的概念设备号的注册和释放静态方法动态方法区别2 设备文件操作struct file_operations与struct file、struct inode关系3 分配和注册字符设备class_createcdev_adddevice_create4 字符设备驱动程序字符设备通过字符&#xff08;一个接一个的字…

Java LinkedHashMap getOrDefault()方法与示例

LinkedHashMap类的getOrDefault()方法 (LinkedHashMap Class getOrDefault() method) getOrDefault() method is available in java.util package. getOrDefault()方法在java.util包中可用。 getOrDefault() method is used to get the value associated with the given key el…

Java中的异常栈轨迹和异常链

Java中允许对异常进行再次抛出&#xff0c;以提交给上一层进行处理&#xff0c;最为明显的例子为Java的常规异常。 常规异常&#xff1a;有Java所定义的异常&#xff0c;不需要异常声明&#xff0c;在未被try-catch的情况下&#xff0c;会被默认上报到main()方法。 Example: pu…

贪心算法---背包问题(物品可以分割问题)

问题背景&#xff1a; 有一天&#xff0c;阿里巴巴赶着一头毛驴上山砍柴。砍好柴准备下山时&#xff0c;远处突然出现一股烟尘&#xff0c;弥漫着直向上空飞扬&#xff0c;朝他这儿卷过来&#xff0c;而且越来越近。靠近以后&#xff0c;他才看清原来是一支马队&#xff0c;他…

同步---信号量

信号量1 信号量2 驱动程序和测试程序3 内核的具体实现总结1 信号量 Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个已经被占用的信号量时&#xff0c;信号量会将其放到一个等待队列&#xff0c;然后让其睡眠&#xff0c;这时处理器去执行其他代码。当持有信号量的进…

Java Float类floatToIntBits()方法与示例

Float类floatToIntBits()方法 (Float class floatToIntBits() method) floatToIntBits() method is available in java.lang package. floatToIntBits()方法在java.lang包中可用。 floatToIntBits() method follows IEEE 754 floating-point standards and according to standa…

解释三度带和六度带的概念以及各坐标系如何定义

★ 地形图坐标系&#xff1a;我国的地形图采用高斯&#xff0d;克吕格平面直角坐标系。在该坐标系中&#xff0c;横轴&#xff1a;赤道&#xff0c;用&#xff39;表示&#xff1b;纵轴&#xff1a;中央经线&#xff0c;用&#xff38;表示&#xff1b;坐标原点&#xff1a;中央…

0-1背包问题(物品不可分割)

问题背景&#xff1a; 所谓“钟点秘书”&#xff0c;是指年轻白领女性利用工余时间为客户提供秘书服务&#xff0c;并按钟点收取酬金。“钟点秘书”为客户提供有偿服务的方式一般是&#xff1a;采用电话、电传、上网等“遥控”式 服务&#xff0c;或亲自到客户公司处理部分业务…

算法---KMP算法

字符串1 KMP算法状态机概述构建状态转移1 KMP算法 原文链接&#xff1a;https://zhuanlan.zhihu.com/p/83334559 先约定&#xff0c;本文用pat表示模式串&#xff0c;长度为M&#xff0c;txt表示文本串&#xff0c;长度为N&#xff0c;KMP算法是在txt中查找子串pat&#xff0…

cache初接触,并利用了DataView

我们在写代码的时候,如果数据控件要获得数据,一般方法,Conn.Open();OleDbCommand cmd;cmd new OleDbCommand(sql, Conn);GridView1.DataSource dbcenter.accessGetDataSet(sql);GridView1.DataBind();Conn.close();但如果多个数据控件要绑定数据,则比较频繁打开数据库,效率一…

Java ByteArrayInputStream reset()方法及示例

ByteArrayInputStream类reset()方法 (ByteArrayInputStream Class reset() method) reset() method is available in java.util package. reset()方法在java.util包中可用。 reset() method is used to reset this ByteArrayInputStream to the last time marked position and …

回文数猜想

问题描述&#xff1a; 一个正整数&#xff0c;如果从左向右读&#xff08;称之为正序数&#xff09;和从右向左读&#xff08;称之为倒序数&#xff09;是一样的&#xff0c;这样的数就叫回文数。任取一个正整数&#xff0c;如果不是回文数&#xff0c;将该数与他的倒序数相加…

文件上传 带进度条(多种风格)

文件上传 带进度条 多种风格 非常漂亮&#xff01; 友好的提示 以及上传验证&#xff01; 部分代码&#xff1a; <form id"form1" runat"server"><asp:ScriptManager ID"scriptManager" runat"server" EnablePageMethods&quo…

同步---自旋锁

1 自旋锁的基本概念 自旋锁最多只能被一个可执行线程持有&#xff0c;如果一个执行线程试图获得一个已经被使用的自旋锁&#xff0c;那么该线程就会一直进行自旋&#xff0c;等待锁重新可用。在任何时刻&#xff0c;自旋锁都可以防止多余一个的执行线程同时进入临界区。 Linu…