博客主页:PannLZ
系列专栏:《Linux系统之路》
欢迎关注:点赞收藏✍️留言
__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)
,解释如下。
*ptr
的数据类型匹配,或者至少可以进行隐式转换。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;
}
与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;
}
write()
方法用于向设备发送数据,每当用户应用程序调用设备文件上的write函数时,就会调用其内核实现。该函数的原型如下:
ssize_t(*write)(struct file *filp, const char __user *buf, size_t count,loff_t *pos);
- 返回值是写入的字节数(长度)。
- *buf表示来自用户空间的数据缓冲区。
- count是请求传输的数据长度。
- ·pos表示数据在文件中应写入的起始位置。
以下步骤既不是实现驱动程序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;
}
read()
方法的原型如下:
ssize_t (*read) (struct file *filp, char __user*buf, size_t count,loff_t *pos);
返回值是读取的数据量。这个方法中相关参数的描述如下:
- *buf:从用户空间接收的缓冲区。
- count:请求传输的数据大小(用户缓冲区的大小)。
- *pos;表示从文件中读取数据的起始位置。
//(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;
}
在文件中移动光标位置时,会调用llseek函数。这个方法在用户空间中的入口点是lseek()
。
loff_t(*llseek) (structfile *filp, loff_toffset, int whence);
反回值是文件中的新位置。
- loff_t是相对于文件当前位置的偏移量,定义当前位置将改变多少。
- whence定义从哪里开始查找,可能取值如下。
- SEEK_SET:光标移动相对于文件开头的位置。
- SEEK_CUR:光标移动相对于当前文件的位置。
- SEEK_END:光标移动相对于文件结束的位置。
//(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$