探索Linux内核空间文件IO实现

时间:2021-08-27 23:38:04

http://ecm943ef.iteye.com/blog/1351396

http://ecm943ef.iteye.com/blog/1351396


探索Linux内核空间文件IO实现
2011年05月04日
  http://blog.csdn.net/wby0322/archive/2010/11/11/6002249.aspx
  本文参考《深入理解Linux内核》完成,详情请查阅相关书籍
  前言
  将磁盘上的文件读入到内存中,将内存中的一段数据写到磁盘的文件上或另一个存储设备。看似很简单的事情,却有一些出人意料的东西蕴含其中。此篇文章介简单绍了在用户空间与内核空间两个不同内存区域下访问文件的方式。
  误区1:C程序语言利用标准C函数库中的相关函数可以实现文件的打开关闭读写等操作,因此可以随意使用。
  解释:linux操作系统的内存管理将内存空间划分为用户态和内核态(即常规的1G大小的内核空间和3G大小的用户空间)。标准C函数库中的文件操作函数只能运行在用户空间,无权访问到内核空间。因此在诸如驱动一类的内核程序中,要实现文件的操作,是不可以使用标准C函数库中的函数的。而应该使用内核提供的文件操作函数。
  误区2:为了在内核中实现标准函数库中的函数,编译时候试图将stdio.h等标准函数的头文件加入到内核的include中。
  注释:你即使这么做也是不会成功的,这些标准函数在用户空间编程时可以使用,但是是无法编译到内核中的。与其实相类似的功能,内核已经提供了足够多函数调用来实现。日后再遇到标准函数库的问题,要首先去查询一下内核是否提供了你想实现的功能函数。
  误区3:include/linux文件夹中的头文件是通用的
  注释:include中的头文件可以说大部分是通用的,但是也有相当一部分不是通用的,是与体系结构密切相关的,比如说锁操作,比如说内存与I/O操作。这些函数在底层的实现都是汇编一级的,而汇编代码本身就与体系结构密切相关。设计到与体系结构相关的头文件,往往优先使用存储在arch/xxx/lnclude/asm中的。另外建议熟悉一下lnclude的文件夹构成,有些头文件并不放在include/linux文件夹下面。
  接下来介绍在用户空间下访问文件和在内核空间下访问文件的两种不用类型操作集。
  第一部分 用户空间下的文件访问
  (参考谭浩强 《C语言程序设计》――第十二章 文件,相关概念讲解的比较详细)
  用户空间文件操作函数注释
  读写文件的常规流程
  第1步:打开文件
  第2步:将文件读入到一段内存中
  第3步:将一段内存中的数据写入到另一个文件中。
  常见的文件类型:字符型 or 二进制型
  在C语言中,上面的流程,可以由以下四个函数集合来实现
  ◆ 打开文件:fopen(file_path,flag);
  ◆ 获取文件大小:stat(file_path,&file); (long)file.st_size;
  ◆ 读取文件到内存中:fread(buf,file_size,count,fp);
  ◆ 将内存中的文件写入到文件:fwrite(buf,file_size,count,fp);
  函数 fopen(file_path,flag)
  功能:打开一个文件
  函数原型:fopen(file_path,flag)
  返回值:指向所打开文件的文件指针
  参数:
  file_path:此处为文件所在绝对路径(包含文件名)
  flag:读写标志位,只读"r"、只写"w"、追加"a"、只读(二进制) "rb"、只写(二进制) "wb"、追加(二进制) "wb"、读写(先读后写) "r+"、读写(先写后读) "w+"、读写(追加) "a+"、读写(二进制先读后写) "rb+"、读写(二进制先写后读) "wb+"、读写(二进制追加) "ab+".
  示例:
  FILE *fp; //用户空间定义一个文件类型的指针
  fp = fopen(/root/test.txt, "r"); //以只读方式打开文件/root/test.txt,并将文件指针返回给fp.函数 stat(file_path,&file)
  功能之一:获取文件大小
  参数:
  file_path:文件路径
  &file:stat类型的结构体的地址,用来保存文件信息
  示例:
  int filesize;
  struct stat file; //定义一个stat类型的结构体变量 file
  stat(file_path,&file); //取得文件信息并将其保存在结构体file中
  filesize=(long)file.st_size; //stat类型的结构变量file中的成员st_size记录着文件大小,我们获取它并将其赋给filesize,供其它函数使用。函数 fread(buf,file_size,count,fp)
  功能:将文件读取到内存中的一个地方
  参数:
  buf:开辟的一块内存空间首地址,将用来存放读入的文件
  file_size:指定读入的字节数
  count:读入指定字节数的次数
  fp:读入的文件的文件指针(你要读的那个文件的文件指针,由fopen获得)
  示例
  buf = (int *)malloc(file_size+1000);
  //在用户空间申请一块内存,大小为将要读取的文件大小(由st_size获得),且预留1000字节的边界。并返回内存的首地址。以供fread使用。//
  fread(buf,file_size,1,fp); //将fp指向的文件,大小为file_size,读入到内存中,保存在内存中的地址为buf,且读入一次。函数 fwrite(buf,file_size,count,fp)
  功能:将内存中的一段数据写入到文件中
  参数:
  buf:将要被写入到文件的那段内存的起始地址
  file_size:数据写入大小
  count:按file_size大小的写入次数
  fp:被写入的文件的文件指针
  示例
  fwrite(buf,file_size,1,fp); //将内存中位置在buf的一段数据,写入到fp所指向的文件中,写入大小为file_size,写入次数为1次。在用户空间获取文件大小有很多种方式,这里选用了stat函数。读写文件也有多种操作函数,这里选用的函数fread fwrite主要以二进制形式对文件进行操作。关于fopen的flag参数,是否为二进制影响不大。
  示例代码
  Eclipse上跑通的代码如下。因为涉及的参数比较多,为了清楚地重现重要步骤,对每步骤的函数进行了简单的封装。参数的传递只要理解上面的介绍即可区分清楚。
  /*
  * hello_file.c
  *
  * Created on: 2010-11-9
  * Author: Wang BaoYi(zats)
  * Email:wby0322@gmail.com
  */
  #include
  #include
  #include
  /* read_file_size封装了获取文件大小的函数
  * 参数为文件路径(包含文件名)。
  * 返回值为文件真实大小,单位字节,长整型。
  */
  static long int read_file_size(char *file_path)
  {
  struct stat file;
  stat(file_path,&file);
  return (long)file.st_size;
  }
  /*read_file_to_mem封装了读取文件到内存的函数
  *参数为(读入文件大小,文件路径(包含文件名))。
  *返回值为存储该文件的内存指针。
  */
  void *read_file_to_mem(int file_size,char *file_path)
  {
  int *buf;
  FILE *fp;
  if((fp = fopen(file_path,"r")) == NULL) //判断文件是否打开    {
  printf("Can not open this file\n");
  exit(0);
  }
  else
  {
  buf = (int *)malloc(file_size+1000);
  fread(buf,file_size,1,fp);
  }
  return buf;
  }
  /* write_mem_to_file封装了将内存数据写入到文件的函数
  * 参数为(目标内存地址,写入数据大小,写入文件名称)。
  * 返回值为0。
  */
  static int write_mem_to_file(int *buf,int file_size,char *file_name)
  {
  FILE *fp;
  if((fp = fopen(file_name,"w+")) == NULL)
  {
  printf("Can not open this file\n");
  exit(0);
  }
  else
  {
  fwrite(buf,file_size,1,fp);
  }
  return 0;
  }
  /*
  * 主函数操作内容
  * 获取文件大小
  * 读文件
  * 将读到的内容写到新文件中
  */
  int main(void)
  {
  int file_size;
  int *buf;
  file_size = read_file_size("/nfs/file_read_test");
  buf = read_file_to_mem(file_size,"/nfs/file_read_test");
  write_mem_to_file(buf,file_size,"/nfs/new_file1");
  printf("%ld\n",read_file_size("/nfs/hello_semaphore.ko"));
  printf("( ̄ε(# ̄) Hello file.\n");
  return 0;
  }
  第二部分 内核空间下的文件访问
  在讲解操作方法前先介绍一些内核相关的概念,后续的针对文件的操作都是基于这些概念的。因此搞清楚前因后果来龙去脉对理解内核空间文件操作很关键。
  关于linux系统内核空间的保护以及用户空间与系统空间数据传递的问题
  虚存中系统空间范围以及用户空间范围:以32位地址为例,32地址意味着4G字节的虚存空间,Linux内核将这4G字节的空间分成两部分。将最高的1G字节(从虚地址0xC0000000至0xFFFFFFFF),用于内核本身,称为“系统空间”也称为内核空间。而将较低的3G字节(从虚地址0x00000000到0xBFFFFFFF),用作各个进程的“用户空间”。
  LINUX建立进程的时候建立了两套内存段描述符,在文件
  linux-2.6.32/arch/xxx/include/asm/segment.h有说明.
  以ARM为例
  #ifndef __ASM_ARM_SEGMENT_H
  #define __ASM_ARM_SEGMENT_H
  #define __KERNEL_CS 0x0
  #define __KERNEL_DS 0x0
  #define __USER_CS 0x1
  #define __USER_DS 0x1
  #endif /* __ASM_ARM_SEGMENT_H */__KERNEL部分为内核段, __USER部分为用户段。在内核空间运行代码的时候调用内核段描述符号就可以直接访问用户空间,但在用户空间运行用户代码的时候用内核段描述符则不能访问内核空间,实现了内核区数据的保护,属于内核的保护机制。
  理论上在用户代码调用系统函数的时候如sys_open,程序进入了系统内核代码,描述符也已经切换到了内核的描述符,这时可以直接访问用户空间或者内核空间,两者的参数数据传递也很简单,可以直接拷贝。但看了LINUX代码的都知道,系统函数代码里面的用户空间与内核空间参数传递是没有这么直接拷贝的,那是为什么呢?大家想一想,用户调用的一些指针参数等,可以指向内核空间,如果不加以检测直接拷贝,那么用户空间代码就可以通过系统调用读写内核空间了,这显然是不准许的。所以内核代码里面就采用了统一的一些函数用于内核空间和用户空间的数据传递:
  copy_from_user
  copy_to_user
  __generic_copy_from_user
  __gerneric_copy_to_user
  在这些函数里面实现用户调用传递的指针合法性检测,这样编写内核代码的时候只要调用这些函数就能实现了对内核空间的保护,编写也比较方便。这就提醒大家自己编写内核代码的时候,千万不要图方便直接用户空间与内核空间的参数拷贝。
  我们再仔细看看这些COPY函数是怎么实现的内核空间保护呢。原来是在每个线程信息的数据结构struct thread_info里面保存了一个用户空间范围,用current_thread_info()宏可以获取当前进程的用户空间范围current_thread_info()->addr_limit,因为内核空间在用户空间上面,所以只要简单检测用户传递参数访问的空间是不是小于等于这个范围就是了。下面是相关的几个文件的相关内容:
  文件uaccess.h(/linux-2.6.32.11/arch/arm/include/asm/)中的部分片段
  /*
  * Note that this is actually 0x1,0000,0000
  */
  #define KERNEL_DS 0x00000000
  #define get_ds() (KERNEL_DS)
  #define USER_DS TASK_SIZE
  #define get_fs() (current_thread_info()->addr_limit)
  static inline void set_fs(mm_segment_t fs)
  {
  current_thread_info()->addr_limit = fs;
  modify_domain(DOMAIN_KERNEL, fs ? DOMAIN_CLIENT : DOMAIN_MANAGER);
  }注意get_ds() get_fs()两个宏的定义,本质是获取的内存段标识符的操作。得到的值的一个用处就是标识性的限定“进程”被允许操作的地址空间所属内存区域段(用户空间/内核空间),实现这个作用的手段就是通过set_fs定义进程描述符中current_thread_info()->addr_limit项。(跟修改权限差不多)。
  文件thread_info.h (/linux-2.6.32.11/arch/arm/include/asm/)中定义了类型mm_segment_t用来保存段描述符变量
  typedef unsigned long mm_segment_t;另外在与体系结构相关的memory.h中会定义物理地址偏移
  #ifdef CONFIG_1GB
  #define PAGE_OFFSET_RAW 0xC0000000
  #elif defined(CONFIG_2GB)
  #define PAGE_OFFSET_RAW 0x80000000
  #elif defined(CONFIG_3GB)
  #define PAGE_OFFSET_RAW 0x40000000
  #endif
  // 上述宏可以配置用户空间与内核空间比例上面提到的那些COPY函数中一些参数所代表的变量只存在于用户空间 (往往有__usr的标识),那么内核调用这些函数并传参时是如何操作的呢。方法是:将当前进程的允许操作地址空间标志addr_limit (默认限定在用户空间上)限定在内核空间中,调用完了过后恢复。
  下面代码就是实现过程
  {
  ...
  mm_segment_t old_fs; //定义一个mm_segment_t类型的变量old_fs
  old_fs = get_fs();
  //将当前进程的地址限定标志即current_thread_info()->addr_limit保存在old_fs变量中
  set_fs(KERNEL_DS);
  //将原本是用户空间下的current_thread_info()->addr_limit标识,改成内核空间,利用set_fs函数
  ...
  open(); //此部分操作的文件,变量全部搬到内核空间中
  read();
  ...
  set_fs(old_fs_value); //段恢复
  ...//此部分设计操作系统的内存管理,简单理解即可。
  }
  内核空间文件操作函数注释
  内核空间读写文件的常规操作步骤同用户空间一样
  第一步:打开文件,获取文件指针
  第二步:将文件读入到一段内存中
  第三步:将一段内存中的数据写入到另一个文件中。
  完成上述功能要用的内核函数有:
  ◆打开文件filp_open()
  ◆关闭文件filp_close()
  ◆读文件内容到内存中vfs_read()
  ◆写内存中的数据到文件vfs_write()
  函数 filp_open(const char* filename, int open_mode, int mode)
  函数功能:在内核空间中打开文件
  函数原形:
  strcut file* filp_open(const char* filename, int open_mode, int mode);
  返回值:strcut file*结构指针,供后继函数操作使用,该返回值用IS_ERR()来检验其有效性。
  参数:
  filename:表明要打开或创建文件的名称(包括路径部分)。
  open_mode:文件的打开方式,O_RDONLY 只读打开、O_WRONLY 只写打开、O_RDWR 读写打开、O_CREAT 文件不存在则创建。
  mode:创建文件时使用,设置创建文件的权限,其它情况可以匆略设为0
  示例
  struct file *file = NULL;
  file = filp_open(/root/test.txt,O_RDWR|O_CREAT,0);
  //以读写方式(没有则创建)打开文件/root/test.txt。并返回test.txt的文件指针给file.函数 filp_close(struct file*filp, fl_owner_t id)
  函数功能:关闭之前打开文件
  函数原型:int filp_close(struct file*filp, fl_owner_t id);
  参数:
  struct file*filp:打开文件的文件指针
  fl_owner_t id:一般传递NULL值,也可用current->files作为实参。
  示例
  filp_close(file, NULL); //关闭指针为file的文件。函数 vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
  函数功能:读取已经打开的文件到内存中
  函数原型:
  ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
  {
  ssize_t ret;
  if (!(file->f_mode & FMODE_READ)) //判断文件是否可读
  return -EBADF;
  if (!file->f_op || (!file->f_op->read && !file->f_op->aio_read)) //是否定义文件读方法
  return -EINVAL;
  if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))
  return -EFAULT;
  ret = rw_verify_area(READ, file, pos, count); //读校验 ,
  if (ret >= 0)
  {
  count = ret;
  if (file->f_op->read)
  ret = file->f_op->read(file, buf, count, pos); //调用文件读操作方法
  else
  ret = do_sync_read(file, buf, count, pos); //通用文件模型读方法
  if (ret > 0)
  {
  fsnotify_access(file->f_path.dentry);
  add_rchar(current, ret);
  }
  inc_syscr(current);
  }
  return ret;
  }通过filp_open我们已经可以在当前进程的文件描述表中找到了file , 于是我们就可以调用保存在file中的文件操作方法(file_operation) file->f_op->read(file, buf, count, pos)来具体的操作文件。
  上面的代码实现并不复杂,在做了一些条件判断以后,如果该文件索引节点inode定义了文件的读实现方法的话,就调用此方法。Linux下特殊文件读往往是用此方法, 一些伪文件系统如:proc,sysfs等,读写文件也是用此方法。而如果没有定义此方法就会调用通用文件模型的读写方法.它最终就是读内存,或者需要从存储介质中去读数据.
  参数:
  struct file *file:打开的文件返回的文件指针,(读的目标文件)
  char __user *buf:在用户空间开辟的一段内存空间的首地址,用来保存文件数据。
  size_t count:指定读取文件中的多少内容。单位字节
  loff_t *pos:文件起始位置偏移值,若从文件头读取,则偏移值为0.可以在文件自身的信息中获取
  示例
  int *buf;
  loff_t *pos = &(file->f_pos);
  buf = (int *)kmalloc(fsize+100,GFP_KERNEL);
  //分配一个文件自身大小+100字节边界的内存空间,将用来存放打开的文件,内存分配方式为kmalloc的flag标志GFP_KERNEL。
  vfs_read(file, buf, fsize, pos); //读文件(指针为file)到内存(buf为起始地址)中,读取字节数定为文件自身大小,偏移为自身.函数 vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
  函数功能:将内存中的一段数据写到文件中
  函数原形:
  ssize_t vfs_write(struct file *file, const char __user *buf, size_t count, loff_t *pos)
  {
  ssize_t ret;
  if (!(file->f_mode & FMODE_WRITE))
  return -EBADF;
  if (!file->f_op || (!file->f_op->write && !file->f_op->aio_write))
  return -EINVAL;
  if (unlikely(!access_ok(VERIFY_READ, buf, count)))
  return -EFAULT;
  ret = rw_verify_area(WRITE, file, pos, count);
  if (ret >= 0)
  {
  count = ret;
  if (file->f_op->write)
  ret = file->f_op->write(file, buf, count, pos);
  else
  ret = do_sync_write(file, buf, count, pos);
  if (ret > 0)
  {
  fsnotify_modify(file->f_path.dentry);
  add_wchar(current, ret);
  }
  inc_syscw(current);
  }
  return ret;
  }可以看出这个函数和vfs_read()是差不多的,只是调用的文件操作方法不同而已(file->f_op->write) ,如果没有定义file->f_op->write ,同样也需要do_sync_write()调用同样文件写操作, 首先把数据写到内存中,然后在适当的时候把数据同步到具体的存储介质中去.
  参数:
  struct file *file:打开的文件返回的文件指针,(写的目标文件)
  char __user *buf:数据在内存中的位置,以该地址为起始的一段内存数据将要写到文件中
  size_t count:指定写入文件中的多少内容。单位字节
  loff_t *pos:文件起始位置偏移值,若从文件头读取,则偏移值为0.可以在文件自身的信息中获取
  示例
  loff_t *pos = &(file->f_pos);
  vfs_write(file,buf,fsize,pos);获取文件的大小
  我们可以利用文件的inode结构获得文件的大小,参考代码如下
  struct file *file = NULL;
  struct inode *inode = NULL;
  file = filp_open(file_path,O_RDWR|O_CREAT,0);
  inode = file->f_dentry->d_inode;
  fsize = inode->i_size;
  printk(KERN_ALERT "size=%d\n",(int)fsize);示例代码
  此ko模块代码在arm架构的fpga上已经跑通。因为涉及的参数比较多,为了清楚地重现重要步骤,对每步骤的函数进行了简单的封装。参数的传递只要理解上面的介绍即可区分清楚。执行流程在static int hello_init(void)函数中
  /*
  * kernel_hello_file.c
  *
  * Created on: 2010-11-9
  * Author: Wang BaoYi(zats)
  * Email:wby0322@gmail.com
  */
  #include
  #include
  #include
  #include
  #include
  #include
  #include
  #include
  #include
  #define FILE_PATH_READ "/file_read_test"
  //打开文件路径(包括文件名),未来将要读的
  #define FILE_PATH_WRITE "/new_file_test"
  //打开文件路径(包括文件名),未来将要写的
  struct file *file = NULL; //保存打开文件的文件指针变量
  struct inode *inode = NULL; //为了获取文件大小用的inode结构变量
  int *file_buf; //保存开辟的内存空间的地址的指针变量
  loff_t fsize; //保存文件大小的变量
  mm_segment_t old_fs; //保存内存边界的变量
  /*
  * kernel_file_open封装了文件打开函数
  * 参数为文件路径(包含文件名)。
  * 操作file类型结构变量。
  * 打开方式为读写(没有则创建)
  */
  static int kernel_file_open(char *file_path)
  {
  file = filp_open(file_path,O_RDWR|O_CREAT,0);
  if (IS_ERR(file))
  {
  printk("Open file %s failed.\n", file_path);
  return 0;
  }
  }
  /*
  * kernel_file_size封装了获取文件大小函数
  * 参数为待获取大小的文件指针。
  * 操作inode类型结构变量。
  * 返回值为文件大小,单位字节
  */
  static loff_t kernel_file_size(struct file *file)
  {
  inode = file->f_dentry->d_inode;
  fsize = inode->i_size;
  printk(KERN_ALERT "size=%d\n",(int)fsize);
  return fsize;
  }
  /*
  * kernel_addr_limit_expend封装了内存边界扩展函数
  * 参数无。
  */
  static int kernel_addr_limit_expend(void)
  {
  old_fs = get_fs();
  set_fs(KERNEL_DS);
  return 0;
  }
  /*
  * kernel_addr_limit_resume封装了内存边界恢复函数
  * 参数无。
  */
  static int kernel_addr_limit_resume(void)
  {
  set_fs(old_fs);
  }
  /*
  * kernel_file_read封装了读文件函数
  * 参数为open的文件指针,获取的文件大小
  * 返回值为读入到内存中的首地址。
  */
  void *kernel_file_read(struct file *file,loff_t fsize)
  {
  int *buf;
  loff_t *pos = &(file->f_pos);
  buf = (int *)kmalloc(fsize+100,GFP_KERNEL);
  vfs_read(file, buf, fsize, pos);
  return buf;
  }
  /*
  * kernel_file_ write封装了读文件函数
  * 参数为open的文件指针,数据在内存中的地址,写入到文件的字节数
  */
  static int kernel_file_write(struct file *file,int *buf,loff_t fsize)
  {
  loff_t *pos = &(file->f_pos);
  vfs_write(file,buf,fsize,pos);
  }
  /*
  * ko的主函数
  */
  static int hello_init(void) //ko的主函数
  {
  printk(KERN_ALERT "Y(^_^)Y Hello Wang`s file.\n");
  kernel_file_open(FILE_PATH_READ); //打开文件file_read_test
  kernel_file_size(file); //获取file_read_test的大小
  /*read file to mem*/
  kernel_addr_limit_expend(); //边界扩展
  file_buf = kernel_file_read(file,fsize); //读操作
  filp_close(file, NULL); //关闭文件file_read_test
  kernel_addr_limit_resume(); //边界恢复
  /*write mem to file*/
  kernel_file_open(FILE_PATH_WRITE); //打开文件new_file_test,没有则创建
  kernel_addr_limit_expend(); //边界扩展
  kernel_file_write(file,file_buf,fsize); //将前面读到内存中的数据,写入到文件new_file_test中
  filp_close(file, NULL); //关闭文件
  kernel_addr_limit_resume(); //边界恢复
  return 0;
  }
  static void hello_exit(void)
  {
  printk(KERN_ALERT "BYE BYE file Y(^_^)Y\n");
  }
  module_init(hello_init);
  module_exit(hello_exit);
  MODULE_LICENSE("Dual BSD/GPL");
  MODULE_AUTHOR("wby");
  MODULE_DESCRIPTION("A simple hello world Module with File");