接上文:http://t.csdnimg.cn/Ai3GA(注册字符设备完整解析版)
对于修改全局变量后的代码注释:
int mychar_open(struct inode *pnode, struct file *pfile)
{
pfile->private_data =(void *) (container_of(pnode->i_cdev,struct mychar_dev,mydev));//已知成员的地址获得所在结构体变量的地址:container_of(成员地址,结构体类型名,成员在结构体中的名称)
//当驱动程序接收到打开文件的请求(如通过其open方法)时,它有机会为新的file结构体分配一个private_data。这通常涉及到动态分配一个驱动程序特定的数据结构(如struct mychar_dev),并将其地址存储在file->private_data中。之后,每当对该文件执行其他操作(如读、写、关闭等)时,驱动程序都可以通过file->private_data访问到这个数据结构。
printk("open call\n");
return 0;
}在后续操作函数中,需要说明使用的是哪个文件:
struct mychar_dev *pmydev = (struct mychar_dev *)pfile->private_data;
一、ioctl操作实现
ioctl
(Input/Output Control)是一个在 Unix-like 操作系统中的系统调用,用于控制设备或文件的各种操作。它允许用户空间程序与内核空间进行交互,执行一些特定的设备控制、状态查询或其他操作,而不必读写设备文件本身。
long xxx_ioctl (struct file *filp, unsigned int cmd, unsigned long arg);
功能:对相应设备做指定的控制操作(各种属性的设置获取等等)
参数:
- filp:指向open产生的struct file类型的对象,表示本次ioctl对应的那次open
- cmd:用来表示做的是哪一个操作
- arg:和cmd配合用的参数
返回值:成功为0,失败-1
mychar.c:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include "mychar.h"
//#define MYCHAR_DEV_CNT 3
//保存主设备号
int major = 11;
int minor = 0;
int mychar_num = 1;//MYCHAR_DEV_CNT;//次设备号数量
#define BUF_LEN 100
/*
struct cdev mydev;//创建的字符设备的描述信息
char mydev_buf[BUF_LEN];
int curlen = 0;//定义一个数据总是从下标0开始的长度(上面的mydev_buf)
*/
struct mychar_dev
{
struct cdev mydev;
char mydev_buf[BUF_LEN];
int curlen;//curlen表示设备当前可读取的数据长度
};
struct mychar_dev gmydev;
int mychar_open(struct inode *pnode, struct file *pfile)
{
pfile->private_data =(void *) (container_of(pnode->i_cdev,struct mychar_dev,mydev));
//已知成员的地址获得所在结构体变量的地址:container_of(成员地址,结构体类型名,成员在结构体中的名称)
//当驱动程序接收到打开文件的请求(如通过其open方法)时,它有机会为新的file结构体分配一个private_data。这通常涉及到动态分配一个驱动程序特定的数据结构(如struct mychar_dev),并将其地址存储在file->private_data中。之后,每当对该文件执行其他操作(如读、写、关闭等)时,驱动程序都可以通过file->private_data访问到这个数据结构。
printk("open call\n");
return 0;
}
int mychar_release(struct inode *pnode, struct file *pfile)
{
printk("release call\n");
return 0;
}
ssize_t mychar_read(struct file *pfile, char __user *puser, size_t count, loff_t *p_pos)
{ /*结构体表示当前打开的文件
指向用户空间缓冲区的指针,用于存储从设备读取的数据
希望从设备读取的字节数
指向长偏移量的指针,用于指示在文件中的当前读写位置*/
struct mychar_dev *pmydev = (struct mychar_dev *)pfile->private_data;
int size = 0;//表示实际能读到的字节数
int ret = 0;
//如果期望读的字节数大于当前可以读到的字节数
if(count > pmydev->curlen)
{
size = pmydev->curlen;//就把能读到的全都给实际能读到的size
}
else
{
size = count;//将buf里面未读完的数据往0位置放
}
/*用户空间中目标缓冲区的指针 ---内核空间中源数据的指针 ---要复制的字节数
* 用于从内核空间复制数据到用户空间*/
ret = copy_to_user(puser,pmydev->mydev_buf,size);
if(ret)
{
printk("copy_to_read failed\n");
return -1;
}
memcpy(pmydev->mydev_buf,pmydev->mydev_buf + size,pmydev->curlen - size);
//mydev_buf为内核储存地址,加上size后移到之前读完size大小数据的后面,将size大小后面没读完的数据重新放回0初始位置。拷贝的大小为(之前可读取的总长度curlen-已经读取的size大小)。
//curlen = curlen - size;
pmydev->curlen -= size;//然后将curlen往前移动size大小的字节即可回到0位置
return size;//返回实际读的字节
}
//将用户空间的数据拷贝到内核空间,数据本身不变,+const
ssize_t mychar_write(struct file *pfile, const char __user *puser, size_t count, loff_t *p_pos)
{
//从puser将数据拷贝p_pos起始位置的期望写入字节数count大小字节给内核空间
int size = 0;//表示实际能写入的字节数
int ret = 0;
struct mychar_dev *pmydev = (struct mychar_dev *)pfile->private_data;
if(count > BUF_LEN-pmydev->curlen)
{
size = BUF_LEN - pmydev->curlen;// 剩余空间不足以容纳全部请求的数据
}
else
{
size = count;// 剩余空间足够大,可以容纳全部请求的数据
}
ret = copy_from_user(pmydev->mydev_buf + pmydev->curlen,puser,size);
if(ret)
{
printk("copy_from_read failed\n");
return -1;
}
pmydev->curlen += size;// 更新文件位置
return size;
}
long mychar_ioctl(struct file *pfile, unsigned int cmd,unsigned long arg)
{
struct mychar_dev *pmydev = (struct mychar_dev *)pfile->private_data;
//文件指针 pfile、一个命令码 cmd 和一个参数 arg(用户空间地址)
int __user *pret = (int *)arg;
//arg 是一个指向用户空间 int 的指针
int maxlen = BUF_LEN;
int ret = 0;
switch(cmd)
{
case MYCHAR_IOCTL_GET_MAXLEN:
ret = copy_to_user(pret,&maxlen,sizeof(int));
if(ret)
{
printk(KERN_ERR"copy to user MAXLEN failed\n");
return -EFAULT;
}
break;
case MYCHAR_IOCTL_GET_CURLEN:
ret = copy_to_user(pret,&pmydev->curlen,sizeof(int));
if(ret)
{
printk(KERN_ERR"copy to user CURLEN failed\n");
return -EFAULT;
}
break;
default:
printk(KERN_ERR"cmd unknow\n");
return -EINVAL;//表示无效参数
}
return 0;
}
struct file_operations myops = {
.owner = THIS_MODULE,
.open = mychar_open,
.release = mychar_release,
.read = mychar_read,
.write = mychar_write,
.unlocked_ioctl = mychar_ioctl,
};
int __init mychar_init(void)
{
int ret = 0;
//将主次设备号合成一个完整设备号
dev_t devno = MKDEV(major,minor);
//手动申请设备号,若返回值不为0,则自动设置
ret = register_chrdev_region(devno,mychar_num,"qmycharq");
if(ret)
{
ret = alloc_chrdev_region(&devno,minor,mychar_num,"qmycharq");
if(ret)
{
printk("get devno failed\n");
return -1;
}
major = MAJOR(devno);//自动分配的设备号不一定是11,所以需要分离出其主设备号
//因为次设备号都是从0开始,所以不用分离
}
cdev_init(&gmydev.mydev,&myops);//初始化字符设备(cdev)结构体
/*在字符设备驱动的开发中,每个字符设备都需要一个与之对应的 cdev 结构体来表示。
* cdev_init 函数的作用就是初始化这个结构体,
* 并建立起 cdev 和 file_operations 结构体之间的连接
* 以便内核能够知道如何操作这个字符设备。*/
gmydev.mydev.owner = THIS_MODULE;
/*用于将字符设备(通过 cdev 结构体表示)的所有者设置为当前模块
* mydev 是指向 cdev 结构体的指针,而 THIS_MODULE 是一个宏,
* 它在编译时会被替换为指向当前模块 module 结构体的指针*/
/*如果驱动程序是作为模块加载的,它通常会使用 THIS_MODULE 宏来指代当前模块,并将 THIS_MODULE 赋值给设备信息的 owner 字段。这样,内核就能够知道哪个模块是设备的拥有者,并在需要时调用该模块的函数。*/
cdev_add(&gmydev.mydev,devno,1);//将指定字符设备加入到管理内核的hash表中
return 0;
}
void __exit mychar_exit(void)
{
dev_t devno = MKDEV(major,minor);
cdev_del(&gmydev.mydev);
//从内核中移除一个字符设备
unregister_chrdev_region(devno,mychar_num);
//参数:需要释放的设备号区域起始值,第一个设备号
//第二个参数:制定释放设备号数量,即需要释放的设备号区域大小
}
MODULE_LICENSE("GPL");
module_init(mychar_init);
module_exit(mychar_exit);
mychar.h:
#ifndef MY_CHAR_H
#define MY_CHAR_H
#include <asm/ioctl.h>
#define MY_CHAR_MAGIC 'k'
#define MYCHAR_IOCTL_GET_MAXLEN _IOR(MY_CHAR_MAGIC,1,int*)
#define MYCHAR_IOCTL_GET_CURLEN _IOR(MY_CHAR_MAGIC,2,int*)
#endif
testmychar_app.c:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <stdio.h>
#include "mychar.h"
int main(int argc,char *argv[])
{
int fd = -1;
char buf[32] = "";
int max = 0;
int cur = 0;
if(argc < 2)
{
printf("The argument is too few\n");
return -1;
}
fd = open(argv[1],O_RDWR);
if(fd < 0)
{
printf("open %s failed\n",argv[1]);
return 2;
}
ioctl(fd,MYCHAR_IOCTL_GET_MAXLEN,&max);
printf("max len is %d\n",max);
write(fd,"hello",6);
ioctl(fd,MYCHAR_IOCTL_GET_CURLEN,&cur);
printf("cur len is %d\n",cur);
read(fd,buf,32);
printf("buf=%s\n",buf);
close(fd);
fd = -1;
return 0;
}
实现效果:
二、printk
//日志级别
#define KERN_EMERG "<0>" /* system is unusable */
#define KERN_ALERT "<1>" /* action must be taken immediately */
#define KERN_CRIT "<2>" /* critical conditions */
#define KERN_ERR "<3>" /* error conditions */#define KERN_WARNING "<4>" /* warning conditions */
#define KERN_NOTICE "<5>" /* normal but significant condition */
#define KERN_INFO "<6>" /* informational */
#define KERN_DEBUG "<7>" /* debug-level messages */用法:printk(KERN_INFO"....",....)
printk(KERN_INFO"Hello World"); =====> printk("<6>""Hello World") ====> printk("<6>Hello World")
使用方法:dmesg --level=emerg,alert,crit,err,warn,notice,info,debug
三、多个次设备的支持
每一个具体设备(次设备不一样的设备),必须有一个struct cdev来代表它
cdev_init
cdev.owner赋值
cdev_add
以上三个操作对每个具体设备都要进行
multimychar.c:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include "mychar.h"
#define MYCHAR_DEV_CNT 3
//保存主设备号
int major = 11;
int minor = 0;
int mychar_num = MYCHAR_DEV_CNT;//次设备号数量
#define BUF_LEN 100
struct mychar_dev
{
struct cdev mydev;
char mydev_buf[BUF_LEN];
int curlen;//curlen表示设备当前可读取的数据长度
};
struct mychar_dev gmydev_arr[MYCHAR_DEV_CNT];
int mychar_open(struct inode *pnode, struct file *pfile)
{
pfile->private_data =(void *) (container_of(pnode->i_cdev,struct mychar_dev,mydev));
//已知成员的地址获得所在结构体变量的地址:container_of(成员地址,结构体类型名,成员在结构体中的名称)
//当驱动程序接收到打开文件的请求(如通过其open方法)时,它有机会为新的file结构体分配一个private_data。这通常涉及到动态分配一个驱动程序特定的数据结构(如struct mychar_dev),并将其地址存储在file->private_data中。之后,每当对该文件执行其他操作(如读、写、关闭等)时,驱动程序都可以通过file->private_data访问到这个数据结构。
printk("open call\n");
return 0;
}
int mychar_release(struct inode *pnode, struct file *pfile)
{
printk("release call\n");
return 0;
}
ssize_t mychar_read(struct file *pfile, char __user *puser, size_t count, loff_t *p_pos)
{ /*结构体表示当前打开的文件
指向用户空间缓冲区的指针,用于存储从设备读取的数据
希望从设备读取的字节数
指向长偏移量的指针,用于指示在文件中的当前读写位置*/
struct mychar_dev *pmydev = (struct mychar_dev *)pfile->private_data;
int size = 0;//表示实际能读到的字节数
int ret = 0;
//如果期望读的字节数大于当前可以读到的字节数
if(count > pmydev->curlen)
{
size = pmydev->curlen;//就把能读到的全都给实际能读到的size
}
else
{
size = count;//将buf里面未读完的数据往0位置放
}
/*用户空间中目标缓冲区的指针 ---内核空间中源数据的指针 ---要复制的字节数
* 用于从内核空间复制数据到用户空间*/
ret = copy_to_user(puser,pmydev->mydev_buf,size);
if(ret)
{
printk("copy_to_read failed\n");
return -1;
}
memcpy(pmydev->mydev_buf,pmydev->mydev_buf + size,pmydev->curlen - size);
//mydev_buf为内核储存地址,加上size后移到之前读完size大小数据的后面,将size大小后面没读完的数据重新放回0初始位置。拷贝的大小为(之前可读取的总长度curlen-已经读取的size大小)。
//curlen = curlen - size;
pmydev->curlen -= size;//然后将curlen往前移动size大小的字节即可回到0位置
return size;//返回实际读的字节
}
//将用户空间的数据拷贝到内核空间,数据本身不变,+const
ssize_t mychar_write(struct file *pfile, const char __user *puser, size_t count, loff_t *p_pos)
{
//从puser将数据拷贝p_pos起始位置的期望写入字节数count大小字节给内核空间
int size = 0;//表示实际能写入的字节数
int ret = 0;
struct mychar_dev *pmydev = (struct mychar_dev *)pfile->private_data;
if(count > BUF_LEN-pmydev->curlen)
{
size = BUF_LEN - pmydev->curlen;// 剩余空间不足以容纳全部请求的数据
}
else
{
size = count;// 剩余空间足够大,可以容纳全部请求的数据
}
ret = copy_from_user(pmydev->mydev_buf + pmydev->curlen,puser,size);
if(ret)
{
printk("copy_from_read failed\n");
return -1;
}
pmydev->curlen += size;// 更新文件位置
return size;
}
long mychar_ioctl(struct file *pfile, unsigned int cmd,unsigned long arg)
{
struct mychar_dev *pmydev = (struct mychar_dev *)pfile->private_data;
//文件指针 pfile、一个命令码 cmd 和一个参数 arg(用户空间地址)
int __user *pret = (int *)arg;
//arg 是一个指向用户空间 int 的指针
int maxlen = BUF_LEN;
int ret = 0;
switch(cmd)
{
case MYCHAR_IOCTL_GET_MAXLEN:
ret = copy_to_user(pret,&maxlen,sizeof(int));
if(ret)
{
printk(KERN_ERR"copy to user MAXLEN failed\n");
return -EFAULT;
}
break;
case MYCHAR_IOCTL_GET_CURLEN:
ret = copy_to_user(pret,&pmydev->curlen,sizeof(int));
if(ret)
{
printk(KERN_ERR"copy to user CURLEN failed\n");
return -EFAULT;
}
break;
default:
printk(KERN_ERR"cmd unknow\n");
return -EINVAL;//表示无效参数
}
return 0;
}
struct file_operations myops = {
.owner = THIS_MODULE,
.open = mychar_open,
.release = mychar_release,
.read = mychar_read,
.write = mychar_write,
.unlocked_ioctl = mychar_ioctl,
};
int __init mychar_init(void)
{
int ret = 0;
int i = 0;
//将主次设备号合成一个完整设备号
dev_t devno = MKDEV(major,minor);
//手动申请设备号,若返回值不为0,则自动设置
ret = register_chrdev_region(devno,mychar_num,"qmycharq");
if(ret)
{
ret = alloc_chrdev_region(&devno,minor,mychar_num,"qmycharq");
if(ret)
{
printk("get devno failed\n");
return -1;
}
major = MAJOR(devno);//自动分配的设备号不一定是11,所以需要分离出其主设备号
//因为次设备号都是从0开始,所以不用分离
}
for(i=0;i<MYCHAR_DEV_CNT;i++)
{
devno = MKDEV(major,minor+i);
cdev_init(&gmydev_arr[i].mydev,&myops);//初始化字符设备(cdev)结构体
gmydev_arr[i].mydev.owner = THIS_MODULE;
cdev_add(&gmydev_arr[i].mydev,devno,1);//将指定字符设备加入到管理内核的hash表中
}
return 0;
}
void __exit mychar_exit(void)
{
dev_t devno = MKDEV(major,minor);
int i = 0;
for(i=0;i<MYCHAR_DEV_CNT;i++)
{
cdev_del(&gmydev_arr[i].mydev);
//从内核中移除一个字符设备
}
unregister_chrdev_region(devno,mychar_num);
//参数:需要释放的设备号区域起始值,第一个设备号
//第二个参数:制定释放设备号数量,即需要释放的设备号区域大小
}
MODULE_LICENSE("GPL");
module_init(mychar_init);
module_exit(mychar_exit);
Makefile:
ifeq ($(KERNELRELEASE),)
ifeq ($(ARCH),arm)
KERNELDIR ?= /home/linux/fs4412/linux-3.14
ROOTFS ?= /opt/4412/rootfs
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
endif
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules INSTALL_MOD_PATH=$(ROOTFS) modules_install
clean:
rm -rf *.o *.ko .*.cmd *.mod.* modules.order Module.symvers .tmp_versions
else
CONFIG_MODULE_SIG=n
#obj-m += myhello.o
#obj-m += xyz.o
#xyz-objs = test.o func.o
obj-m += testparam.o
obj-m += mychar.o
obj-m += multimychar.o
endif
运行结果: