目录
- 1、完整的字符设备驱动的模板
- 2、相关函数
- 2.1 container_of()
- 2.2 kmalloc
- 2.3 copy_to_user()
- 2.4 copy_from_user()
- 2.5 系统调用open()
- 2.6 系统调用read
- 2.7 系统调用write
- 2.8 系统调用close
- 3、实例:多个同类字符设备的驱动
- 3.1 驱动代码
- 3.2 测试
1、完整的字符设备驱动的模板
用一个内存块做为假设的设备。字符设备驱动实现这个内存块的读写等操作。应用程序能够通过对应的设备文件实现对这个设备(内存块)的读写等操作。
2、相关函数
2.1 container_of()
已知成员的地址,要获得所在结构体变量的地址,使用这个宏: container_of(局部成员实际地址,结构体类型名,成员在结构体中的名称)
宏的定义如下:
#include <linux/kernel.h>
#define container_of(ptr, type, member) ({
const typeof(((type *)0)->member) * __mptr = (ptr);
(type *)((char *)__mptr - offsetof(type, member)); })
解释:
第一句const typeof(((type *)0)->member) * __mptr = (ptr);算出了这个局部成员变量的地址,给了_mptr。
第二句offsetof(type,member)是计算出了member在type类型 中的偏移量(假设为s个byte)
(char *)__mptr是把这个地址强转成指向单个byte的地址,为了接下来计算地址量。
接下来((char *)__mptr - offsetof(type, member))就是当前的这个局部成员的地址往回减s,就得出了局部成员变量所在的结构体的起始地址
2.2 kmalloc
#include <linux/slab.h>
函数原型:void *kmalloc(size_t size, gfp_t flags);
kmalloc() 申请的内存位于直接映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因为存在较简单的转换关系,所以对申请的内存大小有限制,不能超过128KB。
较常用的 flags(分配内存的方法):
- GFP_ATOMIC —— 分配内存的过程是一个原子过程,分配内存的过程不会被(高优先级进程或中断)打断;(异常上下文内用这个标志)
- GFP_KERNEL —— 正常分配内存;(会阻塞,所以用于任务上下文)
- GFP_DMA —— 给 DMA 控制器分配内存,需要使用该标志(DMA要求分配虚拟地址和物理地址连续)。
对应的内存释放函数为:
void kfree(const void *objp);
void *kzalloc(size_t size, gfp_t flags) // kmalloc的近亲函数,分配并设0值
2.3 copy_to_user()
#include <asm/uaccess.h>
copy_to_user()函数用于内核空间向用户空间拷贝数据。它的原型如下:
c
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
参数解释:
-
to:指向用户空间缓冲区的指针。
-
from:指向内核空间源缓冲区的指针。
-
n:要拷贝的字节数。
返回值: -
返回值0:表示成功拷贝n个字节数据到内核空间。
-
返回值大于0小于等于n:表示拷贝失败,返回值为未成功拷贝的字节数
这个函数主要在内核驱动中被使用,当驱动向用户空间传递数据时需要调用这个函数。例如一个读函数,它从设备读取数据到内核空间缓冲区,然后调用copy_to_user()将数据拷贝给用户提供的缓冲区。
示例:
c
//内核驱动代码
ssize_t driver_read(char __user *buf, size_t count)
{
char kernel_buf[100];
unsigned long ret;//从设备读取数据到kernel_buf
…//将数据拷贝给用户空间buf
ret = copy_to_user(buf, kernel_buf, count);
if (ret > 0) {
//拷贝失败
return -EFAULT;
}return count; //成功读取count个字节
}
这个函数需要注意几点:
- to指针指向的用户空间内存区域必须由用户进程分配和释放,内核不能直接访问用户空间内存。
- n表示内核空间要拷贝给用户空间的最大字节数。实际拷贝的字节数可能小于n,返回值表示实际拷贝的字节数。
- 如果返回值大于等于n,表示拷贝失败,需要返回错误码。
- 拷贝失败的原因可能是to指向的地址不合法,或者在拷贝过程中出现故障。
- from和to所指内存区域不能重叠。
2.4 copy_from_user()
#include <asm/uaccess.h>
copy_from_user()函数用于用户空间向内核空间拷贝数据。它的原型如下:
c
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
参数解释:
-
to:指向内核空间目标缓冲区的指针。
-
from:指向用户空间源缓冲区的指针。
-
n:要拷贝的字节数。
返回值: -
返回值0:表示成功拷贝n个字节数据到内核空间。
-
返回值大于0小于等于n:表示拷贝失败,返回值为未成功拷贝的字节数
这个函数也主要在内核驱动中被使用,当驱动需要获取用户空间数据传递到内核时需要调用这个函数。例如一个写函数,它调用copy_from_user()从用户空间获取数据,然后将数据写入设备。
示例:
c
//内核驱动代码
ssize_t driver_write(const char __user *buf, size_t count)
{
char kernel_buf[100];
unsigned long ret;//从用户空间拷贝数据到kernel_buf
ret = copy_from_user(kernel_buf, buf, count);
if (ret > 0) {
//拷贝失败
return -EFAULT;
}//将kernel_buf中的数据写入设备
…return count; //成功写入count个字节
}
这个函数也需要注意几点:
- from指针指向的用户空间内存区域必须由用户进程分配和释放,内核不能直接访问用户空间内存。
- n表示要从用户空间拷贝到内核空间的最大字节数。实际拷贝的字节数可能小于n,返回值表示实际拷贝的字节数。
- 如果返回值大于等于n,表示拷贝失败,需要返回错误码。
- 拷贝失败的原因可能是from指向的地址不合法,或者在拷贝过程中出现故障。
- from和to所指内存区域不能重叠。
2.5 系统调用open()
- open函数打开的文件有两种类型:
- 普通文件:文件路径名指向的是一个普通文件,打开后可以读写等操作。
- 特殊文件:文件路径名指向的不是一个普通文件,比如终端设备/dev/tty,管道/dev/pipe等。打开特殊文件后对它的读写代表与该设备的读写通信。
- flags参数表示以什么模式打开文件:
- O_RDONLY:只读方式打开,调用open的进程可以读取该文件,但不能写入。
- O_WRONLY:只写方式打开,调用进程可以写入该文件,但不能读取。
- O_RDWR:读写方式打开,调用进程可以同时读写该文件。
- O_APPEND:以追加方式打开,每次写入会追加到文件末尾。
- O_CREAT:如果该文件不存在,就创建该文件。
- O_EXCL:与O_CREAT配合使用,如果文件已存在,则返回错误。
- O_TRUNC:如果文件已存在,并且以只写或读写方式打开,则先将文件长度截断为0。
- …
这些flags可以使用|操作符组合使用。
- mode参数仅在O_CREAT被指定时有效,它表示创建文件的访问权限。一般用0777。
- 成功打开返回文件描述符(大于等于0的整数),失败返回-1。
- 打开的文件需要调用close关闭。
示例代码:
#include <sys/stat.h>
#include <fcntl.h>
int main() {
//只读方式打开文件
int fd1 = open(“file.txt”, O_RDONLY);
//只写方式打开文件,如果文件不存在则创建
int fd2 = open("file.txt", O_WRONLY | O_CREAT, 0777);
//读写方式打开文件,如果文件已存在则先清空内容
int fd3 = open("file.txt", O_RDWR | O_TRUNC);
//读写方式打开文件,如果文件不存在则创建,且只能由调用进程打开
int fd4 = open("file.txt", O_RDWR | O_CREAT | O_EXCL, 0777);
//.........
close(fd1);
close(fd2);
//......
}
2.6 系统调用read
read函数用于从已打开的文件描述符fd读取数据。它的原型如下:
ssize_t read(int fd, void *buf, size_t count);
参数解释:
- fd:文件描述符,由open函数返回。
- buf:指向用户缓冲区的指针,用于存放读取的数据。
- count:要读取的最大数据量(以字节为单位)。
返回值: - 成功读取的字节数。可能小于count,如果返回0,表示已读取到文件尾。
- -1 表示读取失败。
示例:
c
#include <unistd.h>
int main() {
int fd = open(“file.txt”, O_RDONLY);
char buf[100];
ssize_t count;
//读取100个字节到buf,并获取实际读取的字节数
count = read(fd, buf, 100);
//使用buf中的数据.........
//再次读取100个字节,如果文件结尾则返回0
count = read(fd, buf, 100);
//.......
close(fd);
}
read函数在读取文件时有几点需要注意:
- read读取的是文件内容,不是字符串,所以读取的数据不是以’\0’结束。
- 读取的数据量可能小于请求的count,这时只获取实际读取的字节数。需要重复调用read直到返回0,表示读取完毕。
- read读取文件时,文件读取指针会动。所以下次读取会从上次结束的地方开始。可以使用lseek函数改变读取指针的位置。
- 对于特殊文件(管道,终端等)的读取,read获取的数据来自该特殊文件对应设备的输入。
- 文件描述符fd需要是有效的,对已经close的文件调用read会导致错误。
2.7 系统调用write
write函数用于向已打开的文件描述符fd写入数据。它的原型如下:
c
ssize_t write(int fd, const void *buf, size_t count);
参数解释:
- fd:文件描述符,由open函数返回。
- buf:指向用户缓冲区的指针,该缓冲区包含要写入的数据。
- count:要写入的最大数据量(以字节为单位)。
返回值: - 成功写入的字节数。可能小于count。
- -1 表示写入失败。
示例:
c
#include <unistd.h>
int main() {
int fd = open(“file.txt”, O_WRONLY | O_CREAT, 0777);
char *buf = “Hello World!”;
ssize_t count;
//写入12个字节到文件
count = write(fd, buf, 12);
//再次写入,如果文件不够大可能不会写入12个字节
count = write(fd, buf, 12);
close(fd);
}
write函数在写入文件时有几点需要注意:
- write写入的不是字符串,所以写入的数据不会自动追加’\0’结束符。
- 写入的数据量可能小于请求的count,这时只会写入实际的字节数。需要重复调用write直到写入全部数据。
- write写入文件时,文件写入指针会动。所以下次写入会从上次结束的地方开始。可以使用lseek函数改变写入指针的位置。
- 对于特殊文件(管道,终端等)的写入,write提供的数据会输出到该特殊文件对应设备的输出。
- 文件描述符fd需要是有效的,对已经close的文件调用write会导致错误。
2.8 系统调用close
close()函数用于关闭一个已打开的文件描述符。它的原型如下:
c
#include <unistd.h>
int close(int fd);
参数fd表示要关闭的文件描述符。
返回值:
- 成功返回0
- 失败返回-1,并设置errno
这个函数通常用在文件操作完成后,关闭打开的文件。例如:
c
int fd = open(“file.txt”, O_RDONLY);
// do something with fd
close(fd);
close()函数需要注意以下几点:
- fd必须是一个有效的文件描述符,否则close()将失败并设置errno为EBADF。
- 每个成功的open()或dup()调用都需要一个对应的close()来关闭文件。如果忘记关闭文件描述符,将导致资源泄露。
- close()成功调用后,fd不再引用 opened 文件的任何资源,并可在后续的open()调用中重新使用。
- 关闭 stdin、stdout 或 stderr 流的后果是未定义的。这三个流在程序开始时由内核自动打开,并在程序结束时自动关闭。
- close()可以在不关闭fd的情况下被重复调用,这种情况下将不进行任何操作并返回成功。
- 除非将fd显式声明为int型,否则调用close()时fd的类型需要与open()中的类型相同。
- 在进程终止时,所有未关闭的描述符都被自动关闭。但应该养成明确关闭不再需要的文件描述符的好习惯。
- close()可能会销毁文件打开时指定的任何记录锁。关闭文件描述符后,任何涉及该描述符的记录锁都变得无效。
- 如果文件是以O_APPEND模式打开的,close()将会丢弃追加模式,并将文件描述符的文件位置指针重置为文件开始。
3、实例:多个同类字符设备的驱动
本实例用于一次驱动同种类型的多个字符型设备(主设备号相同,次设备号不同)。以一个内存缓存虚拟一个字符型设备,驱动模块实现对内存缓存虚拟设备的读写操作。
3.1 驱动代码
/*************************************************************************
> File Name: muti-oper-mem.c
用一个内存池来虚拟设备,驱动模块完成对内存池的读与写。应用层程序通过系统调用open
read write操作设备muti0,muti1,muti2。
本例用于驱动同一类设备的不同子设备。主设备号相同,次设备号不同。
与单一设备驱动不同之处在以下几点:
1、mem设备要对应有多个
2、cdev设备对象一一对应
3、devno统一用alloc_chrdev_region()生成
4、每一个cdev设备都要cdev_init和cdev_add
4、每一个cdev都要cdev_del()
************************************************************************/
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <asm/uaccess.h>
/*1、定义重要的变量及结构体*/
#define MEM_SIZE 500 //
#define DEVICE_NUM 3
dev_t devno;
struct mem_dev_t{
struct cdev my_dev; //cdev设备描述结构体变量
char mem[MEM_SIZE]; //内存池,当成虚拟设备
};
/*这个mem_dev_t指针类型,可以指向数组,在这里是指向三个元素的首元素的地址。*/
struct mem_dev_t *mem_dev;
/*所有驱动函数声明*/
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);
//驱动操作函数结构体,成员函数为需要实现的设备操作函数指针
//简单版的模版里,只写了open与release两个操作函数。
struct file_operations fops={
.open = open,
.release = release,
.read = read,
.write = write,
};
/*3、初始化 cdev结构体的函数,并将cdev结构体与file_operations结构体关联起来*/
/*这样在内核中就有了设备描述的结构体cdev,以及设备操作函数的调用集合file_operations结构体*/
//static int cdev_setup(struct mem_dev_t *mem_dev ){
static int cdev_setup(struct mem_dev_t *mem_dev , int num ){
int unsucc = 0;
dev_t sdevno = MKDEV(MAJOR(devno),num);
cdev_init(&mem_dev->my_dev , &fops);
mem_dev->my_dev.owner = THIS_MODULE;
/*4、注册cdev结构体到内核链表中*/
unsucc = cdev_add(&mem_dev->my_dev , sdevno , 1);
if (unsucc){
printk("cdev [%d] add failed \n",num);
return -1;
}
printk("the major of this devno is %d\n",MAJOR(sdevno));
printk("the minor of this devno is %d\n",MINOR(sdevno));
return 0;
}
static int __init my_init(void){
int major ;
int i =0;
int unsucc =0;
//mem_dev = kzalloc(sizeof(struct mem_dev_t) , GFP_KERNEL);
mem_dev = kzalloc(sizeof(struct mem_dev_t) * DEVICE_NUM , GFP_KERNEL);
if (!mem_dev){
printk(" allocating memory failed");
return -1;
}
/*2、创建 devno */
//unsucc = alloc_chrdev_region(&mem_dev->devno , 0 , 1 , "operate_memory");
unsucc = alloc_chrdev_region(&devno , 0 , DEVICE_NUM , "operate_memory");
if (unsucc){
printk(" creating devno failed\n");
return -1;
}else{
major = MAJOR(devno);
printk("make devno,major = %d ; \n",major);
}
/*3、 分别初始化3个cdev结构体,分别关联cdev结构体与file_operations.*/
/*4、注册cdev结构体到内核链表中*/
for ( i = 0 ; i < DEVICE_NUM ; i++){
if (cdev_setup(mem_dev+i , i ) == 0){
printk("the driver operate_memory initalization is complete\n");
} else {
printk("the driver operate_memory initalization failed\n");
return -1;
}
}
return 0;
}
static void __exit my_exit(void)
{
int i=0;
for ( i = 0; i < DEVICE_NUM; i++){
cdev_del(&((mem_dev+i)->my_dev));
}
unregister_chrdev_region(devno , DEVICE_NUM);
printk("***************the driver operate_memory exit************\n");
}
/*5、驱动函数的实现*/
/*file_operations结构全成员函数.open的具体实现*/
int open(struct inode *pnode , struct file *pf){
struct mem_dev_t *p = container_of(pnode->i_cdev, struct mem_dev_t , my_dev);
pf->private_data = p; //把全局变量指针放入到struct file结构体里
printk("operate_memory is opened\n");
return 0;
}
/*file_operations结构全成员函数.release的具体实现*/
int release(struct inode *pnode , struct file *pf){
printk("operate_memory is closed \n");
return 0;
}
/*file_operations结构全成员函数.read的具体实现*/
ssize_t read (struct file * pf, char __user * buf, size_t size , loff_t * ppos){
struct mem_dev_t *pdev = pf->private_data;
int count = 0;
//判断偏移量的有效性
if (*ppos >= MEM_SIZE){
return 0;
}
//判断能够读到的字节数量
if (size > MEM_SIZE - *ppos){
count = MEM_SIZE - *ppos;
}else{
count = size;
}
//copy_from_user返回值大于0失败
if ( copy_to_user(buf , &pdev->mem[*ppos] , count )){
return 0;
}else{
*ppos += count;
return count;
}
}
/*file_operations结构全成员函数.write的具体实现*/
ssize_t write (struct file * pf, const char __user *buf, size_t size , loff_t *ppos){
struct mem_dev_t *pdev = pf->private_data;
int count = 0;
//判断偏移量的有效性
if (*ppos >=MEM_SIZE ){
return 0;
}
//判断能够写入的字节数量
if (size > MEM_SIZE-*ppos){
count = MEM_SIZE-*ppos;
}else{
count = size;
}
//copy_from_user返回值大于0失败
if ( copy_from_user(&pdev->mem[*ppos] , buf , count)){
return 0;
}else{
*ppos +=count;
return count;
}
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("");
以上代码可以做为模版使用。针对一个或多个同类设备来开发类似驱动时,可以使用。
3.2 测试
对地以上这个驱动,不论要使用几个设备,都只需要加载一次该驱动即可。对应不同的设备,需要创建不同的多个设备文件。如本例是3个同类设备。因此就要手动创建3个设备文件(mknod),主设备号一致,次设备号从0开始到2。
使用时,可以用系统命令“> ” 、cat等操作设备文件,也可以写一段应用程序通过系统调用open() 、read()、write()等。具体见下方操作。
一、加载驱动
insmod /drv/muti-oper-memory.ko
二、手动生成设备文件
mknod /dev/muti0 c 251 0
mknod /dev/muti1 c 251 1
mknod /dev/muti2 c 251 2
三、测试
echo “hello /dev/muti0” > /dev/muti0
cat /dev/muti0
四、应用层测试程序用例
/*************************************************************************
> File Name: muti_op_mem.c
对应驱动的应用层测试程序。调用open read write 等 来测试驱动的运行
分别打开三个驱动进行读写,测试是否正常
执行:./muei_op_mem.elf /dev/muti0 /dev/muti1 dev/muti2
************************************************************************/
#include<stdio.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#define DEVICE_NUM 3
int main(int argc , char **argv){
int fd[DEVICE_NUM] = {0};
int size = 0;
char buf[20] = {0};
char * mesg[3] ={"this is test0\n" , "this is test1\n" , "this is test2\n"};
int pos = 0;
int i=0;
if (argc < DEVICE_NUM+1){
printf("argument is less!\n");
return 0;
}
//向设备写入数据
for (i = 0; i < DEVICE_NUM; i++){
printf("open file %s\n",argv[i+1]);
fd[i] = open(argv[i+1] , O_RDWR|O_APPEND );
if (fd[i] < 0){
perror("open ");
}
size = write(fd[i] ,mesg[i] , strlen(mesg[i]));
printf("write device[%d] size = %d\n",i,size);
if (size == strlen(mesg[i])){
printf("write to device[i] is sruccessul\n",i);
}else{
printf("write to device[i] is failed\n",i);
}
sleep(1);
//printf("sleep is end \n");
close(fd[i]);
//从设备读出数据
fd[i] = open(argv[i+1] , O_RDONLY);
if (fd[i] < 0){
perror("open");
}
size = read(fd[i] , buf , strlen(mesg[i]));
if (size > 0){
printf("read data of device[%d] is : %s\n" ,i, buf);
}else{
printf("read data of device[%d] failed\n",i);
}
sleep(1);
//printf("sleep is end \n");
close(fd[i]);
}
return 0;
}
输出结果如下: