Linux字符设备驱动基础

Linux内核对设备的分类

Linux将文件分为:(括号内为 ls 命令查看的标识符)

  • 普通文件(-)
  • 目录文件(d)
  • 管道文件(p)
  • 本地socket文件(s)
  • 链接文件(l)
  • 字符设备(c)
  • 块设备(b)

Linux内核按驱动程序实现模型框架的不同,将设备分为三类:

  1. 字符设备:按字节流形式进行数据读写的设备,一般情况下按顺序访问,数据量不大,一般不设缓存
  2. 块设备:按整块进行数据读写的设备,最小的块大小为512字节(一个扇区),块的大小必须是扇区的整数倍,Linux系统的块大小一般为4096字节(4k byte),随机访问设缓存以提高效率
  3. 网络设备:针对网络数据收发的设备

设备号

内核用设备号来区分同类里不同的设备,设备号是一个无符号32位整数,数据类型为dev_t,设备号分为两部分:(下图为ls -l查看文件的设备号)

  1. 主设备号:占高12位,用来表示驱动程序相同的一类设备
  2. 次设备号:占低20位,用来表示被操作的哪个具体设备

应用程序打开一个设备文件时,通过设备号来查找定位内核中管理的设备。

在程序代码中,可以通过MKDEV宏用来将主设备号和次设备号组合成32位完整的设备号,用法:

dev_t devno;
int major = 251;//主设备号
int minor = 2;//次设备号
devno = MKDEV(major,minor);

可以通过MAJOR宏用来从32位设备号中分离出主设备号,用法:

dev_t devno = MKDEV(249,1);
int major = MAJOR(devno);

可以通过MINOR宏用来从32位设备号中分离出次设备号,用法:

dev_t devno = MKDEV(249,1);
int minor = MINOR(devno);

如果已知一个设备的主次设备号,应用层指定好设备文件名,那么可以用mknod命令在/dev目录创建代表这个设备的文件,即此后应用程序对此文件的操作就是对其代表的设备操作,mknod用法如下:

@ cd /dev
@ mknod 设备文件名 设备种类(c为字符设备,b为块设备)  主设备号  次设备号    //非超级用户需加sudo执行

在应用程序中如果要创建设备可以调用系统调用函数mknod,其原型如下:

int mknod(const char *pathname,mode_t mode,dev_t dev);
/* 
pathname:带路径的设备文件名,无路径默认为当前目录,一般都创建在/dev下
mode:文件权限 位或 S_IFCHR/S_IFBLK
dev:32位设备号
返回值:成功为0,失败-1 
*/

设备号的申请和注销

字符驱动开发的第一步是通过模块的入口函数向内核添加本设备驱动的代码框架,主要步骤为:

  1. 申请设备号
  2. 定义代表本设备的结构体元素
  3. 初始化代表本设备的结构体元素
  4. 向内核添加代表本设备的结构体元素

使用的主要接口:

int register_chrdev_region(dev_t from, unsigned count, const char *name)

功能:手动分配设备号,先验证设备号是否被占用,如果没有则申请占用该设备号

参数:

    from:自己指定的设备号

    count:申请的设备数量

    name:/proc/devices文件中与该设备对应的名字,方便用户层查询主设备号

返回值:

   成功为0,失败负数,绝对值为错误码

int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count, const char *name)

功能:动态分配设备号,查询内核里未被占用的设备号,如果找到则占用该设备号

参数:

    dev:分配设备号成功后用来存放分配到的设备号

    baseminior:起始的次设备号,一般为0

    count:申请的设备数量

    name:/proc/devices文件中与该设备对应的名字,方便用户层查询主次设备号

返回值:

    成功为0,失败负数,绝对值为错误码

分配成功后在**/proc/devices**可以查看到申请到主设备号和对应的设备名,mknod时参数可以参考查到的此设备信息

void unregister_chrdev_region(dev_t from, unsigned count)

功能:释放设备号

参数:

    from:已成功分配的设备号将被释放

    count:申请成功的设备数量

释放后/proc/devices文件对应的记录消失;

下面是一段示例代码(在内存空间中模拟一个字符设备),以及其编译出来的ko文件执行了insmod命令和rmmod之后,/proc/devices中的变化

示例代码:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>

int major = 11;
int minor = 0;
int mychar_num = 1;

int __init mychar_init(void)
{
    int ret = 0;
	dev_t devno = MKDEV(major,minor);

	ret = register_chrdev_region(devno, mychar_num, "mychar"); //初次进行手动分配设备号
	if (ret)
	{
		ret = alloc_chrdev_region(&devno, minor, mychar_num, "mychar"); //如果手动分配失败,自动分配设备号
		if (ret)
		{
			printk("get devno failed\\n");
			return -1;
		}
		major = MAJOR(devno);
	}
    return 0;
}
   
void __exit mychar_exit(void)
{
    dev_t devno = MKDEV(major,minor);

    unregister_chrdev_region(devno, mychar_num);
}
MODULE_LICENSE("GPL");
module_init(mychar_init);
module_exit(mychar_exit);

编译出来后,执行sudo insmod mychar,之后执行cat /proc/devices | grep mychar

可以得到输出结果:

11 mychar

说明手动分配的设备号成功,如果前面的数字不同,说明是手动分配失败,由Linux自动分配的设备号;

执行sudo rmmod mychar,之后执行cat /proc/devices | grep mychar可以发现没有输出了,说明设备已经被移除;

注册字符设备

一个字符设备类型具备如下成员变量:

struct cdev
{
	struct kobject kobj;//表示该类型实体是一种内核对象
	struct module *owner;//填THIS_MODULE,表示该字符设备从属于哪个内核模块
	const struct file_operations *ops;//指向空间存放着针对该设备的各种操作函数地址
	struct list_head list;//链表指针域
	dev_t dev;//设备号
	unsigned int count;//设备数量
};

我们在字符设备驱动当中需要定义struct cdev类型的变量作为字符设备进行操作;

定义一个struct cdev设备有两种方法:

  1. 直接定义:定义结构体全局变量
  2. 动态申请:
struct  cdev * cdev_alloc()

之后通过函数cdev_init()将字符设备与设备操作进行绑定:

void cdev_init(struct cdev *cdev,const struct file_operations *fops)

其中的struct file_operations对象中的各个函数指针成员都对应相应的系统调用函数

struct file_operations 
{
   struct module *owner;           //填THIS_MODULE,表示该结构体对象从属于哪个内核模块
   int (*open) (struct inode *, struct file *);	//打开设备
   int (*release) (struct inode *, struct file *);	//关闭设备
   ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);	//读设备
   ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);    //写设备
   loff_t (*llseek) (struct file *, loff_t, int);		//定位
   long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);//读写设备参数,读设备状态、控制设备
   unsigned int (*poll) (struct file *, struct poll_table_struct *);	//POLL机制,实现多路复用的支持
   int (*mmap) (struct file *, struct vm_area_struct *); //映射内核空间到用户层
   int (*fasync) (int, struct file *, int); //信号驱动
   //......
};

应用层通过调用系统函数来间接调用这些函数指针成员指向的设备驱动函数,下图简单表示了系统调用和设备操作函数的调用关系:

完成字符设备的初始化(cdev_init())之后,我们通过以下两个函数将字符设备添加至内核/从内核中移除字符设备:

int cdev_add(struct cdev *p,dev_t dev,unsigned int count)
/*
功能:将指定字符设备添加到内核
参数:
	p:指向被添加的设备
	dev:设备号
	count:设备数量,一般填1
*/
void cdev_del(struct cdev *p)
/*
功能:从内核中移除一个字符设备
参数:
	p:指向被移除的字符设备
*/

阶段小结

字符设备驱动开发步骤:

  1. 如果设备有自己的一些控制数据,则定义一个包含struct cdev成员的结构体struct mydev,其它成员根据设备需求,设备简单则直接用struct cdev;
  2. 定义一个struct mydev或struct cdev的全局变量来表示本设备;也可以定义一个struct mydev或struct cdev的全局指针(记得在init时动态分配);
  3. 定义三个全局变量分别来表示主设备号、次设备号、设备数;
  4. 定义一个struct file_operations结构体变量,其owner成员置成THIS_MODULE;
  5. module init函数流程:a. 申请设备号 b. 如果是全局设备指针则动态分配代表本设备的结构体元素 c. 初始化struct cdev成员 d. 设置struct cdev的owner成员为THIS_MODULE e. 添加字符设备到内核;
  6. module exit函数:a. 注销设备号 b. 从内核中移除struct cdev c. 如果如果是全局设备指针则释放其指向空间;
  7. 编写各个操作函数并将函数名初始化给struct file_operations结构体变量;

以下是一段基本的示例代码与验证代码,包括验证的步骤(示例代码是基于上文的设备号代码进行的添加):

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>

int major = 11;
int minor = 0;
int mychar_num = 1;

struct cdev mydev;

int mychar_open(struct inode *pnode, struct file *pfile)
{
	printk("mychar_open is called\\n");
	return 0;
}

int mychar_close(struct inode *pnode, struct file *pfile)
{
	printk("mychar_close is called");
	return 0;
}

struct file_operations myops = {
	.owner = THIS_MODULE,
	.open = mychar_open,
	.release = mychar_close
};

int __init mychar_init(void)
{
    int ret = 0;
    dev_t devno = MKDEV(major,minor);

    //Request a device number
    ret = register_chrdev_region(devno, mychar_num, "mychar");
    if (ret)
    {
        ret = alloc_chrdev_region(&devno, minor, mychar_num, "mychar");
        if (ret)
        {
            printk("get devno failed\\n");
            return -1;
        }
        major = MAJOR(devno);
    }

    //Bind character device operations
    cdev_init(&mydev, &myops);
    //Add a character device to the kernel
    mydev.owner = THIS_MODULE;
    cdev_add(&mydev, devno, mychar_num);

    return 0;
}
   
void __exit mychar_exit(void)
{
    dev_t devno = MKDEV(major,minor);

    cdev_del(&mydev);

    unregister_chrdev_region(devno, mychar_num);
}
MODULE_LICENSE("GPL");
module_init(mychar_init);
module_exit(mychar_exit);

insmod这段代码编译之后获得的ko模块(下文以mychar.ko表示)

sudo insmod mychar.ko 
cat /proc/devices | grep mychar

确认编写的mychar这个字符设备创建成功后,创建一个设备节点,方便执行open和close的设备操作验证

sudo mknod /dev/mydev c 11 0

编写一段测试代码,去对mychar设备进行open和close的操作

#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 = -1;

	if(argc < 2)
	{
		printf("The argument is too few\\n");
		return 1;
	}

	fd = open(argv[1], O_RDONLY);
	if(fd < 0)
	{
		printf("open %s failed\\n", argv[1]);
		return 2;
	}

	close(fd);
	fd = -1;
	return 0;
}

将上述这段测试代码编译执行(笔者测试代码文件命名为test_mychar.c)

gcc test_mychar.c -o test_mychar -Wall
./test_mychar /dev/mydev

再通过dmesg查看内核运行log

可以看到测试程序对字符设备进行操作后,输出了驱动内设定好的内核打印信息

字符设备驱动框架简介

内核中记录文件元信息的结构体

struct inode
{
	//....
	dev_t  i_rdev;//设备号
	struct cdev  *i_cdev;//如果是字符设备才有此成员,指向对应设备驱动程序中的加入系统的struct cdev对象
	//....
}
/*
	1. 内核中每个该结构体对象对应着一个实际文件,一对一
	2. open一个文件时如果内核中该文件对应的inode对象已存在则不再创建,不存在才创建
	3. 内核中用此类型对象关联到对此文件的操作函数集(对设备而言就是关联到具体驱动代码)
*/

读写文件内容过程中用到的一些控制性数据组合而成的对象------文件操作句柄

struct file
{
	//...
	mode_t f_mode;//不同用户的操作权限,驱动一般不用
	loff_t f_pos;//position 数据位置指示器,需要控制数据开始读写位置的设备有用
	unsigned int f_flags;//open时的第二个参数flags存放在此,驱动中常用
	struct file_operations *f_op;//open时从struct inode中i_cdev的对应成员获得地址,驱动开发中用来协助理解工作原理,内核中使用
	void *private_data;//本次打开文件的私有数据,驱动中常来在几个操作函数间传递共用数据
	struct dentry *f_dentry;//驱动中一般不用,除非需要访问对应文件的inode,用法flip->f_dentry->d_inode
    int refcnt;//引用计数,保存着该对象地址的位置个数,close时发现refcnt为0才会销毁该struct file对象
	//...
};
/*
	1. open函数被调用成功一次,则创建一个该对象,因此可以认为一个该类型的对象对应一次指定文件的操作
	2. open同一个文件多次,每次open都会创建一个该类型的对象
	3. 文件描述符数组中存放的地址指向该类型的对象
	4. 每个文件描述符都对应一个struct file对象的地址
*/

字符设备驱动代码流程与框架说明

驱动实现端

驱动使用端

字符设备驱动实现简单读写操作

先上完整代码,这部分代码是在上文的字符设备驱动代码当中添加了两个操作函数mychar_read()和mychar_write(),对应app层的open和read操作:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>

#define BUF_MAX 100

int major = 11;
int minor = 0;
int mychar_num = 1;

struct cdev mydev;

char mydev_buf[BUF_MAX];
int curlen = 0;

ssize_t mychar_read(struct file *pfile, char __user *puser, size_t count, loff_t *p_pos);
ssize_t mychar_write(struct file *pfile, const char __user *puser, size_t count, loff_t *p_pos);

int mychar_open(struct inode *pnode, struct file *pfile)
{
	printk("mychar_open is called\\n");
	return 0;
}

int mychar_close(struct inode *pnode, struct file *pfile)
{
	printk("mychar_close is called");
	return 0;
}

struct file_operations myops = {
	.owner = THIS_MODULE,
	.open = mychar_open,
	.release = mychar_close,
	.read = mychar_read,
	.write = mychar_write
};

ssize_t mychar_read(struct file *pfile, char __user *puser, size_t count, loff_t *p_pos)
{
	int size = 0;
	int ret = 0;

	if(count > curlen)
	{
		size = curlen;
	}
	else
	{
		size = count;
	}

	ret = copy_to_user(puser, mydev_buf, size);
	if(ret)
	{
		printk("copy_to_user failed\\n");
		return -1;
	}

	memcpy(mydev_buf, mydev_buf + size, curlen - size);

	curlen = curlen - size;

	return size;
}

ssize_t mychar_write(struct file *pfile, const char __user *puser, size_t count, loff_t *p_pos)
{
	int size = 0;
	int ret = 0;

	if(count > BUF_MAX - curlen)
	{
		size = BUF_MAX - curlen;
	}
	else
	{
		size = count;
	}

	ret = copy_from_user(mydev_buf + curlen, puser, size);
	if(ret)
	{
		printk("copy_from_user failed\\n");
		return -1;
	}
	curlen = curlen + size;

	return size;
}

int __init mychar_init(void)
{
    int ret = 0;
    dev_t devno = MKDEV(major,minor);

    //Request a device number
    ret = register_chrdev_region(devno, mychar_num, "mychar");
    if (ret)
    {
        ret = alloc_chrdev_region(&devno, minor, mychar_num, "mychar");
        if (ret)
        {
            printk("get devno failed\\n");
            return -1;
        }
        major = MAJOR(devno);
    }

    //Bind character device operations
    cdev_init(&mydev, &myops);
    //Add a character device to the kernel
    mydev.owner = THIS_MODULE;
    cdev_add(&mydev, devno, mychar_num);

    return 0;
}
   
void __exit mychar_exit(void)
{
    dev_t devno = MKDEV(major,minor);

    cdev_del(&mydev);

    unregister_chrdev_region(devno, mychar_num);
}
MODULE_LICENSE("GPL");
module_init(mychar_init);
module_exit(mychar_exit);

分别对mychar_read()和mychar_write()做解析:

mychar_read()

/*
四个参数:
一个指向文件结构体的指针pfile;
一个指向用户空间缓冲区的指针puser
要读取的字节数count
一个指向文件偏移量的指针p_pos
函数返回一个ssize_t类型的值,表示读取的字节数或错误代码。
*/
ssize_t mychar_read(struct file *pfile, char __user *puser, size_t count, loff_t *p_pos)
{
  //定义一个整型变量size并初始化为0,用于记录实际读取的字节数
	int size = 0;
	int ret = 0;

  //判断要读取的字节数count是否大于当前缓冲区中可用的字节数curlen
	if(count > curlen)
	{
		size = curlen;
	}
	else
	{
		size = count;
	}

  //使用copy_to_user函数将设备缓冲区mydev_buf中的数据拷贝到用户空间缓冲区puser中
	ret = copy_to_user(puser, mydev_buf, size);
	if(ret)
	{
		printk("copy_to_user failed\\n");
		return -1;
	}

  //将剩余的数据从缓冲区中移动到起始位置,以便下次读取
	memcpy(mydev_buf, mydev_buf + size, curlen - size);
  //更新当前缓冲区中可用的字节数
	curlen = curlen - size;

	return size;
}

mychar_write()

/*
四个参数:
一个指向文件结构体的指针pfile;
一个指向用户空间缓冲区的指针puser
要读取的字节数count
一个指向文件偏移量的指针p_pos
函数返回一个ssize_t类型的值,表示读取的字节数或错误代码。
*/
ssize_t mychar_write(struct file *pfile, const char __user *puser, size_t count, loff_t *p_pos)
{
	int size = 0;
	int ret = 0;

  //函数会根据当前内核缓冲区中的数据长度(curlen)和要写入的字节数(count)来确定实际要写入的字节数(size)
	if(count > BUF_MAX - curlen)
	{
		size = BUF_MAX - curlen;
	}
	else
	{
		size = count;
	}

  //调用copy_from_user函数将用户空间的数据缓冲区中的数据复制到内核缓冲区中
	ret = copy_from_user(mydev_buf + curlen, puser, size);
	if(ret)
	{
		printk("copy_from_user failed\\n");
		return -1;
	}

  //函数更新内核缓冲区中的数据长度(curlen),将实际写入的字节数作为函数的返回值返回
	curlen = curlen + size;

	return size;
}

最后注意要在struct file_operations中进行绑定,这样app层的write和read操作就可以通过/dev/mydev这个节点进行进行简单的文件读写操作了;

当然,以上操作是将一块内存用作模拟设备的数据进行读写,实际开发中,涉及到具体物理设备的数据读写就需要访问对应的物理设备的寄存器来完成了

字符设备驱动实现简单IO操作

在编写IO操作前,先优化一下原本的代码结构,不过多地去使用全局变量进行操作,将mydev以及其中用到的mydev_buf、curlen封装成一个结构体,再由各个操作函数中定义指针指向它从而完成各种操作;

struct mychar_dev
{
    struct cdev mydev;
    char mydev_buf[BUF_MAX];
    int curlen;
};

优化过后的代码中需要注意的是在mychar_open中新增了一步:

pfile->private_data = (void *)(container_of(pnode->i_cdev, struct mychar_dev, mydev));

解释一下,这一步是通过container_of宏,将定义的struct mychar_dev类型变量的地址获取,并赋值给pfile->private_data;通过pfile将struct mychar_dev类型变量传递给下面各种读写、io操作的函数,pnode、pfile具体内部结构体的成员变量介绍见 字符设备驱动框架简介 章节

CMD组成

  1. dir(direction),ioctl 命令访问模式(属性数据传输方向),占据 2 bit,可以为 _IOC_NONE、_IOC_READ、_IOC_WRITE、_IOC_READ | _IOC_WRITE,分别指示了四种访问模式:无数据、读数据、写数据、读写数据;
  2. size,涉及到 ioctl 函数 第三个参数 arg ,占据 13bit 或者 14bit(体系相关,arm 架构一般为 14 位),指定了 arg 的数据类型及长度,如果在驱动的 ioctl 实现中不检查,通常可以忽略该参数;
  3. type(device type),设备类型,占据 8 bit,在一些文献中翻译为 “幻数” 或者 “魔数”,可以为任意 char 型字符,例如‘a’、’b’、’c’ 等等,其主要作用是使 ioctl 命令有唯一的设备标识;
  4. nr(number),命令编号/序数,占据 8 bit,可以为任意 unsigned char 型数据,取值范围 0~255,如果定义了多个 ioctl 命令,通常从 0 开始编号递增;

下面是IO CMD宏定义原型

#define _IOC(dir,type,nr,size) (((dir)<<_IOC_DIRSHIFT)| \\
                               ((type)<<_IOC_TYPESHIFT)| \\
                               ((nr)<<_IOC_NRSHIFT)| \\
                               ((size)<<_IOC_SIZESHIFT))
/* used to create numbers */

// 定义不带参数的 ioctl 命令
#define _IO(type,nr)   _IOC(_IOC_NONE,(type),(nr),0)

//定义带读参数的ioctl命令(copy_to_user) size为类型名
#define _IOR(type,nr,size)  _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))

//定义带写参数的 ioctl 命令(copy_from_user) size为类型名
#define _IOW(type,nr,size)  _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))

//定义带读写参数的 ioctl 命令 size为类型名
#define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))

/* used to decode ioctl numbers */
#define _IOC_DIR(nr)        (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)
#define _IOC_TYPE(nr)       (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
#define _IOC_NR(nr)     (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
#define _IOC_SIZE(nr)      (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)

io操作函数:

long mychar_ioctl(struct file *pfile,unsigned int cmd ,unsigned long arg)
{
	int __user *pret = (int *)arg;
	int maxlen = BUF_MAX;
	int ret = 0;
	struct mychar_dev *pmydev = (struct mychar_dev *)pfile->private_data;

	swich(cmd)
	{
		case MYCHAR_IOCTL_GET_MAXLEN:
			ret = copy_to_user(pret, &maxlen, sizeof(int));
			if(ret)
			{
				printk("copy_to_user MAXLEN failed\\n");
				return -1;
			}
			break;
		case MYCHAR_IOCTL_GET_CURLEN:
			ret = copy_to_user(pret, &pmydev->curlen, sizeof(int));
			if(ret)
			{
				printk("copy_to_user CURLEN failed\\n");
				return -1;
			}
			break;
		default:
			printk("Unkowned CMD!\\n");
			break;
	}

	return 0;
}

主要功能是通过MYCHAR_IOCTL_GET_MAXLEN和MYCHAR_IOCTL_GET_CURLEN这两个CMD将字符设备最大缓存数据和当前缓存数据的值传递给app;

app端的调用:

ioctl(fd, MYCHAR_IOCTL_GET_MAXLEN, &max);
printf("max len is %d \\n", max);

ioctl(fd, MYCHAR_IOCTL_GET_CURLEN, &cur);
printf("cur len is %d \\n", cur);

完成后进行调试,可以看到终端输出:

附字符驱动的完整代码(mychar.c mychar.h)和app端测试代码

mychar.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include "mychar.h"

#define BUF_MAX 100

int major = 11;
int minor = 0;
int mychar_num = 1;

struct mychar_dev
{
    struct cdev mydev;
    char mydev_buf[BUF_MAX];
    int curlen;
};

struct mychar_dev g_mydev;

ssize_t mychar_read(struct file *pfile, char __user *puser, size_t count, loff_t *p_pos);
ssize_t mychar_write(struct file *pfile, const char __user *puser, size_t count, loff_t *p_pos);
long mychar_ioctl(struct file *pfile,unsigned int cmd ,unsigned long arg);

int mychar_open(struct inode *pnode, struct file *pfile)
{
    pfile->private_data = (void *)(container_of(pnode->i_cdev, struct mychar_dev, mydev));
	printk("mychar_open is called\\n");
	return 0;
}

int mychar_close(struct inode *pnode, struct file *pfile)
{
	printk("mychar_close is called");
	return 0;
}

struct file_operations myops = {
	.owner = THIS_MODULE,
	.open = mychar_open,
	.release = mychar_close,
	.read = mychar_read,
	.write = mychar_write,
	.unlocked_ioctl = mychar_ioctl,
};

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;
	}
	else
	{
		size = count;
	}

	ret = copy_to_user(puser, pmydev->mydev_buf, size);
	if(ret)
	{
		printk("copy_to_user failed\\n");
		return -1;
	}

	memcpy(pmydev->mydev_buf, pmydev->mydev_buf + size, pmydev->curlen - size);

	pmydev->curlen = pmydev->curlen - size;

	return size;
}

ssize_t mychar_write(struct file *pfile, const 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 > BUF_MAX - pmydev->curlen)
	{
		size = BUF_MAX - pmydev->curlen;
	}
	else
	{
		size = count;
	}

	ret = copy_from_user(pmydev->mydev_buf + pmydev->curlen, puser, size);
	if(ret)
	{
		printk("copy_from_user failed\\n");
		return -1;
	}
	pmydev->curlen = pmydev->curlen + size;

	return size;
}

long mychar_ioctl(struct file *pfile,unsigned int cmd ,unsigned long arg)
{
	int __user *pret = (int *)arg;
	int maxlen = BUF_MAX;
	int ret = 0;
	struct mychar_dev *pmydev = (struct mychar_dev *)pfile->private_data;

	switch(cmd)
	{
		case MYCHAR_IOCTL_GET_MAXLEN:
			ret = copy_to_user(pret, &maxlen, sizeof(int));
			if(ret)
			{
				printk("copy_to_user MAXLEN failed\\n");
				return -1;
			}
			break;
		case MYCHAR_IOCTL_GET_CURLEN:
			ret = copy_to_user(pret, &pmydev->curlen, sizeof(int));
			if(ret)
			{
				printk("copy_to_user CURLEN failed\\n");
				return -1;
			}
			break;
		default:
			printk("Unkowned CMD!\\n");
			break;
	}

	return 0;
}

int __init mychar_init(void)
{
    int ret = 0;
    dev_t devno = MKDEV(major,minor);

    //Request a device number
    ret = register_chrdev_region(devno, mychar_num, "mychar");
    if (ret)
    {
        ret = alloc_chrdev_region(&devno, minor, mychar_num, "mychar");
        if (ret)
        {
            printk("get devno failed\\n");
            return -1;
        }
        major = MAJOR(devno);
    }

    //Bind character device operations
    cdev_init(&g_mydev.mydev, &myops);
    //Add a character device to the kernel
    g_mydev.mydev.owner = THIS_MODULE;
    cdev_add(&g_mydev.mydev, devno, mychar_num);

    return 0;
}
   
void __exit mychar_exit(void)
{
    dev_t devno = MKDEV(major,minor);

    cdev_del(&g_mydev.mydev);

    unregister_chrdev_region(devno, mychar_num);
}
MODULE_LICENSE("GPL");
module_init(mychar_init);
module_exit(mychar_exit);

mychar.h

#ifndef __MYCHAR_H__
#define __MYCHAR_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

app端测试代码

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

#include "mychar.h"

int main(int argc, char *argv[])
{
	int fd = -1;
	int max = 0;
	int cur = 0;

	char buf[8] = "";

	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, 8);

	printf("buf=%s\n", buf);

	close(fd);
	fd = -1;
	return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Linux字符设备驱动实验是指在Linux操作系统中编写和测试字符设备驱动程序的过程。字符设备驱动程序负责与字符设备进行交互,包括输入输出数据、控制设备和处理设备的状态等。 在进行Linux字符设备驱动实验之前,首先需要了解字符设备字符设备驱动的基本概念及其工作原理。字符设备是指以字符为单位进行输入输出的设备,如串口、打印机等。字符设备驱动是指将操作系统与字符设备进行交互的程序。 在实验中,我们通常需要编写一个字符设备驱动程序,包括初始化设备、读写数据、控制设备等功能。首先,我们需要定义字符设备驱动的数据结构,包括设备号、驱动程序打开、关闭等函数的实现。然后,我们需要实现字符设备驱动的读写函数来实现数据的输入输出。最后,我们可以进行一些附加功能的实现,如控制设备的状态、处理中断等。 在实验过程中,我们需要使用Linux内核提供的字符设备接口来进行字符设备驱动的编写和测试。可以使用一些工具和命令来加载和测试字符设备驱动程序,如insmod、rmmod等。通过这些工具和命令,我们可以加载和卸载字符设备驱动程序,并在用户空间进行数据的读写操作,来测试字符设备驱动的功能和性能。 Linux字符设备驱动实验可以帮助我们深入了解字符设备字符设备驱动的工作原理,并学习Linux内核的开发和调试技术。通过实验,我们可以更好地理解操作系统和驱动程序之间的关系,提高我们在Linux系统开发和嵌入式系统开发中的能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值