字符设备驱动

字符设备驱动

字符设备通过字符(一个接一个的字符)以流方式向用户程序传递数据,就像串行端口那样。字符设备驱动通过/dev目录下的特殊文件公开设备的属性和功能,通过这个文件可以在设备和用户应用程序之间交换数据,也可以通过它来控制实际的物理设备。这也是Linux的基本概念,一切皆文件。
字符设备在内核中的结构体:
在这里插入图片描述

主设备和次设备

字符设备在/dev目录下,不能简单地把它们当做普通文件。ls -l查看。
在这里插入图片描述
c:字符设备文件
b:块设备文件
l:符号链接
d:目录
s:套接字
p:命名管道
b和c设备第五、第六列分别为主设备号和次设备号。
设备号用dev_t结构体表示
在这里插入图片描述
主设备号在dev_t的前12位,通过>>20读取,次设备号在低20位,通过dev&(1111 1111 1111 1111 1111)读取。

设备注册时,必须使用主设备号和次设备号,前者标识这个设备,后者用作本地设备列表中的数组索引,因为同一个驱动程序的一个实例可以处理多个设备,而不同的驱动程序可以处理相同类型的不同设备。

设备号的分配和释放

动态方法:使用alloc_chrdev_region()函数,使内核为设备自动分配设备号。
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name)
返回值:
函数执行成功返回0,执行失败返回-1.
参数:

  • dev:设备号(输出)
  • firstminor:申请第一个次设备号 (输入,常用从0开始)
  • count:申请的次设备的数量
  • name:相关设备或驱动的名称
    函数的声明:
    在这里插入图片描述
    函数的定义:
    在这里插入图片描述
    新建一个指向字符设计结构体变量指针cd;
    调用__register_chrdev_region注册一定数量次设备号的驱动(写好设备名字);
    设备号从字符设备结构体变量
    cd处获取。
    释放设备号:
1 unregister_chrdev_region(devid, 1); /* 注销设备号*/

–from csdn

设备文件操作
可以在文件上执行的操作在内核中定义为struct file_operations 的实例。
在这里插入图片描述
如果想让用户在设备文件上执行write操作,必须在驱动中实现与write函数相对应的对调函数,并把它添加到绑定在设备上的struct file_operations中。
当用户代码在指定文件上调用与该文件相关的系统调用时,内核会查找负责这个文件的驱动程序,定位他的struct file_operations结构,并检查和该系统调用匹配的方法是否已经定义。如果定义了,就运行它否则返回错误码。
内核中的文件表示
内核把文件描述为 inode 结构的实例:
在这里插入图片描述
struct inode 是文件系统的数据结构,它只与操作系统相关,用于保存文件(无论它的类型是字符、块、管道等)或目录(从内核的角度看,目录也是文件,是其他文件的入口点)信息。
struct file结构是更高级的问价描述,它代表内核中打开的文件,依赖于底层的struct inode数据结构。
在这里插入图片描述
其中:

  • f_path:文件路径
  • *f_op:可以在此文件上执行的操作即struct file_operations实例
  • *f_inode:与此文件相关的inode
    二者之间的区别:
    struct inode代表内核中的文件
    struce file描述实际打开的文件
    同一个文件打开多次时,可能会有不同的文件描述符,但他们都指向同一个inode。
    inode 不跟踪文件的当前位置和当前模式,它知识帮助操作系统找到底层文件结构的内容(管道、目录、常规磁盘文件、块、字符设备文件等)。而struct file则是一个基本结构,它代表打开的文件,并且提供一组函数,他们与底层文件结构体上执行的方法相关(open,cloce…)。

分配和注册描述符
字符设备在内核中表示位struct cdev实例。在编写设备驱动程序时,目标是创建并注册与struct file_operations 关联的结构实例,为用户空间提供一组可以在该设备上执行的操作(函数)。为了实现这个目标,必须执行以下操作:

  • 使用alloc_cdrdev_region()注册一个主设备号和一定范围内的次设备号
  • 使用class_create()创建自己的设计类,该函数在/sys/class中定义。
  • 创建一个struct file_operations(传递给cdev_init),每一个设备都需要创建,并调用call_init和cdev_add()注册这个设备。
  • 调用device_create()创建每个设备,并给它们一个合适的名字。这样,就可在/dev目录下创建出设备。

写文件操作

内核空间和用户空间数据交换
驱动程序的write()方法包括从用户空间读取数据到内核空间,然后在内核中处理这些数据。这一处理就像是把数据推给设备。
另外,驱动程序的read()方法主要是把数据从内核复制到用户空间中。

  • copy_from_user():把缓冲区内容从用户空间复制到内核空间
  • copy_to_user():把缓冲区内容从内核空间复制到用户空间
    在这里插入图片描述
    带_user前缀的指针指向用户空间(不可信)内存。n代表要复制的字节数量,from代表源地址,to代表目的地址。

__user前缀含义

__user表明参数是一个用户空间的指针,不能在kernel代码中直接访问。也方便其它工具对代码进行检查。

https://nanxiao.me/linux-kernel-note-38-__user/

单值复制
当复制像char和int这样的单个简单变量,而不是像结构和数组这样的大数据类型时,内核会提供专用的宏来快速执行所需的操作。

  • put_user(x, ptr):将内核空间变量值复制到用户空间。x表示要复制到用户空间的值,ptr是用户空间的目标地址。该宏成功返回0,失败则返回-EFAULT 。x和ptr必须有相同的类型。
  • get_user(x, ptr):将用户空间的变量值复制到内核空间,成功时返回0,错误时返回-EFAULT。错误时x被设置为0。x表示存储结果的内核变量,ptr是用户空间的原地址。间接引用ptr的结果必须在没有强制类型转换的情况下可复制给x。

open方法
open方法在每次打开设计文件时被调用。如果这个方法没有定义,则设备文件打开总是成功。通常用这个方法来执行设备和数据结构的初始化,如果有错误发生,返回负的错误码,成功返回0.
在这里插入图片描述
函数描述:在设备上执行open时,将把struct inode作为参数传递给该回调函数,inode 是文件在内核底层的表示。inode结构内的i_cdev指向在init函数中分配出来的cdev。
在这里插入图片描述
open操作实例
结构体定义

struct pcf1215{
	strict cdev cdev;
	unsigned char *sram_data;
	struct i2c_client *client;
	int sram_size;
	...
};

open方法

static unsigned int sram_major = 0;
static struct class *sram_class = NULL;
static int sram_open(struct inode *inode, struct file *filp)
{
	unsigned int maj = imajor(inode);
	unsigned int min = iminor(inode);
	struct pcf1215 *pcf = NULL; 
	pcf = container_of(inode->i_dev, struct pcf1215, cdev);
	pcf->sram_size = SRAM_SIZE;

	if(mai != sram_majoe || min<0){
		prerr("device not found\n");
		return -ENODEV;
	}

	if(pcf->sram_data == NULL){
		pcf->sram_data = lzalloc(pcf->sram_size, GFP_KERNEL);
		if(pcf->sram_data == NULL){
			pr_err("open: memory allocation failed\n"0;
			return -ENOMWEN;
		}
	}
	filp->private_data = pcf;
	return 0;
}

open方法过程:

  1. 传入文件节点inode和文件描述符struct file 实例*filp
  2. 从inode处获取主次设备号
  3. 从inode处通过container_of利用inode->i_cdev读取出cdev数据
  4. 将文件描述符filp->private_data指向新获取的数据传出(数据封装在结构体struct pcf1215实例中
  5. 成功则返回0

release方法
与open方法相反,release方法在设备关闭时调用,之后必须撤销在open任务中已经执行的所有操作。所需做的操作大概如下:

  1. 释放在open()阶段分配的所有内存
  2. 关闭设备,并且在最后一次关闭中释放每一个缓冲区(如果设计支持多次打开,或者驱动程序可以处理多个设备)
static int sram_release(struct inode *inode, struct file *filp)
{
	struct pcf1215 *pcf = NULL;
	pcf = container_of(inode->i_dev, struct pcf1215, cdev);
	mutex_lock(&device_list_lock);
	filp->private_data = NULL;

	pcf1215->user--;
	if(!pcf1215->user){
		kfree(tx_buffer);
		kfree(rx_buffer);
		tx_buffer = NULL;
		rx_buffer = NULL;
		...
		if(any_global_struct)
			kfree(any_global_struct);
	}
	mutex_unlock(&device_list_lock);
	return 0;
}

write方法
write方法用于向设备发送数据,每当用户应用程序调用设备文件上的write函数时,就会调用其内核实现。该函数原型如下:
ssize_t (*write) (struct file *, const char __user *buff, size_t count, loff_t *pos);

  • 返回值是写入的字节数
  • *buf表示来自用户空间的数据缓冲区
  • count是请求传输的数据长度
  • *pos表示数据在文件中应写入的起始位置。

read方法
ssize_t (*read) (struct file *filp, char __user *buf, size_t count, loff_t *pos);

  • 返回值是读取的字节数
  • *buf从用户空间接收的缓冲区
  • count是请求传输的数据长度
  • *pos表示数据在文件中应读取的起始位置。
    read()示例;
ssize_t eep_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
	struct eeprom_dev *eep = filp->private_data;
	if(*f_pos >= EEP_SIZE)	/* EOF */
		return 0;
	
	if((*f_ops + count) >= EEP_SIZE)
		count = EEP_SIZE - *f_pos;
	
	/*查找下一个数据字节的位置*/
	int part_origin = PART_SIZE * eep->part_index;
	int eep_reg_addr_start = part_oringin + *pos;
	/*从设备执行读操作*/
	if(read_from_device(eep_reg_addr_start, buff, count) < 0;{
		pr_err("ee241c512:i2c_transfer failed\n");
		return -EFAULT;
	}

	/*从内核复制到用户空间*/
	if(copy_to_user(buf, dev->data, count) != 0)
		return -EIO;
	
	*f_pos += count;
	return count;
}

llseek方法
在文件中移动光标时,会调用llseek函数。这个方法在用户空间中的入口点是llseek()。
loff_t (*llseek) (struct file *filp, loff_t offset, int whence);

  • 返回值是文件中的新位置。
  • loff_t是相对文件当前的偏移量,定义当前位置将改变多少。
  • whence定义了从哪里开始查找,可能取值如下:
    • SEEK_SET:光标移动相对于文件开头的位置。
    • SEEK_CUR:光标移动相对于当前文件的位置。
    • SEEK_END:光标移动相对于文件结束的位置。
      llseek的步骤:
      (1)使用switch语句检查每种whence情况,因为其取值有限,所以还要相应调整newpos:
swutch(whence){
	case SEEK_SET:	/*相对于文件开头的位置*/
		newpos = offset;	/*偏移称为新位置*/
		break;
	case SEEK_CUR:	/*相对于当前文件的位置*/
		newpos = file->f_pos + offset;
		break;
	case SEEK_END:	/*相对于文件结束的位置*/
		newpos = filesize + offset;
		break;
	default:
		return -EINVAL;
	}

(2)检查newpos是否有效:

if(newpos < 0)
	return -EINVAL;

(3)使用新位置更新f_pos:
filp->f_pos = newpos;
(4)返回新的文件指针位置:
return newpos;
实例:

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

#define CHAR_DEVICE "toto.txt"

int main(int argc, char **argv)
{
	int fd = 0;
	char buf[20];
	if((fd = open(CHAR_DEVICE, O_RDONLY)) < -1)
		return 1;
	
	/*读取20字节*/
	if(read(fd, buf, 20) != 20)
		return 1;
	printf("%S\n",buf);

	/*将光标相对于其实际位置移动10次*/
	if(lseek(fd, 10, SEEK_CUR) < 0)
		return 1;
	if(read(fd, buf, 20) != 20)
		return 1;
	printf("%s\n",buf);

	/*将光标相对于文件的起始位置移动10次*/
	if(lseek(fd, 10, SEEK_SET) < 0)
		return 1;
	if(read(fd, buf, 20) != 20)
		return 1;
	printf("%s",buf);
	close(fd);
	return 0;
}

poll方法
如果需要实现被动等待(在感知字符设备时不浪费CPU周期),则必须实现poll()函数,每当用户空间程序在与设备关联的文件上执行系统调用select()或poll()时,都会调用poll()函数:
__poll_t (*poll) (struct file *, struct poll_table_struct *);
这个方法核心的函数是poll_wait(),定义在<linus/poll.h>中,这个头文件应该被包含在驱动代码中:

static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
	if (p && p->_qproc && wait_address)
		p->_qproc(filp, wait_address, p);
}

poll_wait()根据注册到struct poll_table结构(作为第三个参数传递)中的事件,把与struct file结构(作为第一个参数)关联的设备添加到可以唤醒进程的列表(由第二个参数struct wait_queue_head_t 结构指定,进程在其中处于睡眠状态)中。用户进程可以运行poll(),select(),或epoll()系统调用把需要等待的一组文件添加到等待队列上,以了解是否有相关的设备准备就绪。之后,内核将会调用与每个设备文件相关的驱动程序的poll入口。每个驱动程序的poll方法再调用poll_wait(),为需要接受内核通知的进程注册事件,在这些事件发生之前把进程置于睡眠状态,并把驱动程序注册为可以唤醒进程的驱动程序。通常的方法是根据select()(或poll())系统调用的事件,为每个事件类型使用一个等待队列(一方面是考虑可读性,另一方面是考虑可写性,最后是考虑需要时的异常处理)。

poll步骤
当实现poll函数时,read和write方法可能会改变
(1)为每个需要实现被动等待的事件类型(读、写、异常)声明等待队列,当无数可读或设备不可写时,把任务放入该队列:

static DECLARE_WAIT_QUEUE_HEAD(my_wq);
static DECLARE_WAIT_QUEUE_HEAD(my_rq);

(2)实现poll函数

#include<stdio.h>
static unsigned int eep_poll(struct file *file, poll_table *wait)
{
	unsigned int reval_mask = 0;
	poll_wait(file, &my_wq, wait);
	poll_wait(file, &my_rq, wait);

	if(new-data-is-ready)
		reval_mask |= (POLLIN | POLLRDNORM);
	if(ready_to_be_written)
		reval_mask |= (POLLOUT | POLLWRNORM);
	return reval_mask;
}

(3)当有新数据或者是设备可写入时,通知等待队列:

wait_up_interruptible(&my_rq);	/*准备读取*/
wait_up_interruptible(&my_wq);	/*准备写入*/
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值