【Linux】设备驱动——读写文件操作

在这里插入图片描述

博客主页:PannLZ
系列专栏:《Linux系统之路》
欢迎关注:点赞收藏✍️留言

文章目录

    • 1.读写文件操作
      • 1.1内核空间与用户空间数据交换
        • 单值复制
      • 1.2open方法
      • 1.3release方法
      • 1.4write方法
        • write步骤
      • 1.5read方法
        • read步骤
      • 1.6llseek方法
        • llseek步骤


1.读写文件操作

1.1内核空间与用户空间数据交换

__user是一个Sparse使用的cookie(语义检查器,内核用来查找可能的编码错误),让开发人员知道他实际上将要使用不可信指针(也就是在当前虚拟地址映射中可能无效的指针),他不应该间接访问,而应使用专用的内核函数来访问该指针指向的内存。这样能够引入访问内存(读或写)所需的不同内核函数。这些函数是copy_from_user()copy_to_user(),它们分别把缓冲区内容从用户空间复制到内核空间,以及把缓冲区内容从内核空间复制到用户空间

unsigned long copy_from_user(void *to, constvoid __user *from,unsigned long n)
unsigned long copy_to_user(void __user *to,const void *from,unsigned long n)

这两个函数中,带__user前缀的指针指向用户空间(不可信)内存。n代表要复制的字节数量,from代表源地址,to代表目的地址。每个函数的返回值是未复制的字节数,如果成功,则返回值应该是0。

使用copy_to_user()时,如果某些数据无法复制,则该函数将使用零字节将复制的数据填充到请求的大小。

单值复制

当复制像char和int这样的单个简单变量,而不是像结构和数组这样的大数据类型时,内核会提供专用的宏来快速执行所需的操作。这些宏是put_user(x, ptr)get_user(x, ptr),解释如下。

  • put_user(x, ptr);:该宏将内核空间变量值复制到用户空间。x表示要复制到用户空间的值,ptr是用户空间中的目标地址。该宏成功时返回0,出错时返回-EFAULT。x必须可分配给间接引用ptr的结果。换句话说,它们必须有(或指向)相同的类型。
  • get_user(x, ptr);:该宏将用户空间的变量值复制到内核空间,成功时返回0,错误时返回-EFAULT。请注意,错误时x被设置为0。x表示存储结果的内核变量,ptr是用户空间的源地址。间接引用ptr的结果必须在没有强制类型转换的情况下可赋值给x,猜一猜这是什么意思?这是一种编程规范,为了确保目标变量的数据类型与 *ptr 的数据类型匹配,或者至少可以进行隐式转换。

1.2open方法

open方法在每次打开设备文件时被调用。如果这个方法没有定义,则设备文件打开总是成功。通常用这个方法来执行设备和数据结构的初始化,如果有错误发生,则返回负的错误码;否则返回0。open方法的原型定义如下:

int (*open)(struct inode *inode, struct file *filp);

每次在设备上执行open时,将把struct inode作为参数传递给该回调函数,inode是文件在内核底层的表示。struct inode结构内的i_cdev字段指向在init函数中分配出来的cdev

像下面例子中struct pcf2127那样在设备指定数据中嵌入struct cdev,就可以用container_of宏获取指向该指定数据的指针。下面是open方法例子

struct pcf2127 {
		struct cdev cdev;
		unsigned char *sram_data;
		struct i2c_client *client;
		int sram_size;
		[...]
};

static unsigned int sram_major = 0;
static struct class *sram_class = NULL;
static int sram_open(struct inode *inode, structfile *filp)
{
		unsigned int maj = imajor(inode);
		unsigned int min = iminor(inode);
		struct pcf2127 *pcf = NULL;
		pcf = container_of(inode->i_cdev, struct pcf2127, cdev);
		pcf->sram_size = SRAM_SIZE;
		if (maj != sram_major || min < 0 ){
				pr_err ("device not found\n");
				return -ENODEV; /* 没有这样的设备 */
		}
		/* 如果设备是第一次打开,准备缓冲 */
		if (pcf->sram_data == NULL) {
				pcf->sram_data = kzalloc(pcf->sram_size, GFP_KERNEL);
				if (pcf->sram_data == NULL) {
						pr_err("Open: memory allocationfailed\n");
						return -ENOMEM;
				}
		}
		filp->private_data = pcf;
        return 0;
}

1.3release方法

与open方法相反,release方法在设备关闭时调用,之后必须撤销在open任务中已经执行的所有操作。所需做的操作大致如下。
(1)释放在open()阶段分配的所有私有内存。
(2)关闭设备(如果支持关闭),并且丢弃上次关闭时的每个缓冲区(如果设备支持多次打开,或者驱动程序可以同时处理多个备)。

以下摘自release函数:

static int sram_release(struct inode *inode,struct file *filp)
{
		struct pcf2127 *pcf = NULL;
		pcf = container_of(inode->i_cdev, struct pcf2127, cdev);
		mutex_lock(&device_list_lock);
		filp->private_data = NULL;
		/* 最后关闭 */
		pcf2127->users--;
		if (!pcf2127->users) {
				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;
}

1.4write方法

write()方法用于向设备发送数据,每当用户应用程序调用设备文件上的write函数时,就会调用其内核实现。该函数的原型如下:

ssize_t(*write)(struct file *filp, const char __user *buf, size_t count,loff_t *pos);
  • 返回值是写入的字节数(长度)。
  • *buf表示来自用户空间的数据缓冲区。
  • count是请求传输的数据长度。
  • ·pos表示数据在文件中应写入的起始位置。
write步骤

以下步骤既不是实现驱动程序write()标准的方法,也不是其通用方法,它只是概述在该方法中可以执行哪些类型的操作:

1)检查来自用户空间的错误或无效请求。这个
步骤只有在设备提供内存(电可擦编程只读存储器、
I/O内存等)时才有意义,它可能有内存大小限制:
/* 如果试图写入文件末尾之外,则返回错误
* 这里的filesize对应设备内存的大小(如果有的话)
*/
if ( *pos >= filesize ) return -EINVAL;2)针对剩余字节数调整count,以便不超出文件
大小。这一步也不是必需的,与第(1)步中的相同条
件相关:
/* 文件大小强制响应设备内存的大小 */
if (*pos + count > filesize)
count = filesize - *pos;3)找到开始写入的位置。只有当设备具有内
存,并且write()方法要在其中写入指定的数据时,此
步骤才是相关的。和第(2)步一样,这一步不是必需
的:
/* 将pos转换为有效地址*/
void *from = pos_to_address( *pos );4)从用户空间复制数据,并将其写入相应的内
核空间:
if (copy_from_user(dev->buffer, buf, count) !=
0){
retval = -EFAULT;
goto out;
}
/* 现在将数据从dev->buffer移动到物理设备 */5)写入物理设备,在失败时返回错误:
write_error = device_write(dev->buffer, count);
if ( write_error )
return -EFAULT;6)根据写入的字节数移动文件中光标的当前位
置。最后,返回复制的字节数:
*pos += count;
Return count;

以下是一个write方法的例子。,这仅仅是一个概述:

ssize_t
eeprom_write(struct file *filp, const char__user *buf, size_t count,loff_t *f_pos)
{
		struct eeprom_dev *eep = filp->private_data;
		ssize_t retval = 0;
		/*步骤(1)*/
		if (*f_pos >= eep->part_size)
		/* Writing beyond the end of a
		partition is not allowed. */
				return -EINVAL;
		/*步骤(2)*/
		if (*pos + count > eep->part_size)
				count = eep->part_size - *pos;
		/*步骤(3) */
		int part_origin = PART_SIZE * eep->part_index;
		int register_address = part_origin + *pos;
		/*步骤(4) */
		/* 将数据从用户空间复制到内核空间 */
		if (copy_from_user(eep->data, buf, count) !=0)
				return -EFAULT;
		/* 步骤(5)*/
		/* 执行对设备的写操作*/
		if (write_to_device(register_address, buff,count) < 0){
				pr_err("ee24lc512: i2c_transferfailed\n");
				return -EFAULT;
		}
		/* 步骤(6) */
		*f_pos += count;
		return count;
}

1.5read方法

read()方法的原型如下:

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

返回值是读取的数据量。这个方法中相关参数的描述如下:

  • *buf:从用户空间接收的缓冲区。
  • count:请求传输的数据大小(用户缓冲区的大小)。
  • *pos;表示从文件中读取数据的起始位置。
read步骤
//(1)防止读操作超出文件大小,并返回文件结束:
if (*pos >= filesize)
return 0; /* 0 means EOF */

//(2)读取的字节数不能超过文件大小。适当调整count:
if (*pos + count > filesize)
count = filesize - (*pos); 

//(3)找到读取的起始位置:
void *from = pos_to_address (*pos); /* convertpos into validaddress */

//(4)将数据复制到用户空间缓冲区,失败时返回错误:
sent = copy_to_user(buf, from, count);
if (sent)
		return -EFAULT;

//(5)根据读取的字节数前移文件的当前位置,并返回复制的字节数:
*pos += count;
Return count;

以下是一个驱动程序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_pos + count > EEP_SIZE)
				count = EEP_SIZE - *f_pos;
		/* 查找下一个数据字节的位置*/
		int part_origin = PART_SIZE * eep->part_index;
		int eep_reg_addr_start = part_origin +*pos;
		/* 从设备执行读操作 */
		if (read_from_device(eep_reg_addr_start,buff, count) < 0){
				pr_err("ee24lc512: i2c_transferfailed\n");
				return -EFAULT;
		}
		/* 从内核复制到用户空间 */
		if(copy_to_user(buf, dev->data, count) != 0)
		return -EIO;
		*f_pos += count;
		return count;
}

1.6llseek方法

在文件中移动光标位置时,会调用llseek函数。这个方法在用户空间中的入口点是lseek()

loff_t(*llseek) (structfile *filp, loff_toffset, int whence);

反回值是文件中的新位置。

  • loff_t是相对于文件当前位置的偏移量,定义当前位置将改变多少。
  • whence定义从哪里开始查找,可能取值如下。
  • SEEK_SET:光标移动相对于文件开头的位置。
  • SEEK_CUR:光标移动相对于当前文件的位置。
  • SEEK_END:光标移动相对于文件结束的位置。
llseek步骤
//(1)使用switch语句检查每种whence情况,因为其取值有限,所以还要相应调整newpos:
switch( 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;

下面的用户程序例子连续读取和搜索文件,之后底层驱动程序将执行llseek()文件操作:

#include 
#include 
#include 
#include 
#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, 7, SEEK_SET) < 0)
				return 1;
		if (read(fd, buf, 20) != 20)
				return 1;
		printf("%s\n",buf);
		close(fd);
		return 0;
}

/*该代码产生的输出以下:
jma@jma:~/work/tutos/sources$ cat toto.txt
Lorem ipsum dolor sit amet, consectetur   adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua.
jma@jma:~/work/tutos/sources$ ./seek
Lorem ipsum dolor si
nsectetur adipiscing
psum dolor sit amet,
jma@jma:~/work/tutos/sources$

你可能感兴趣的:(Linux系统之路,linux,java,运维,驱动开发,内核开发,Linux内核)