• 嵌入式Linux入门-Linux文件IO讲解并实现copy程序


    嵌入式Linux入门学习教程汇总:嵌入式Linux教程—裸机、应用、驱动完整教程目录

    在Linux系统中,一切都是“文件”:普通文件、驱动程序、网络通信等等。所有的操作,都是通过“文件IO”来操作的。

    IO就是input和output,文件IO就是文件的读写。文件没有打开时是存放在块设备中的文件系统里的,这样的文件叫做静态文件

    操作一个文件一般是先打开(open)一个文件,得到这个文件的文件描述符,然后对文件进行读写(read/write)或其他操作,最后关闭(close)文件。

    当我们open一个文件的时候,内核在进程中建立一个打开文件的数据结构来记录我们打开的文件,然后在内存中申请一段内存,将静态文件的内容读取到内存中特定地址管理存放,此时内存中的就是动态文件

    之后的读写等操作都是针对这份内存中的动态文件,当close关闭动态文件后,内核将内存中动态文件的内容同步更新到静态文件中。

    文件描述符的概念:一个程序打开一个文件就会得到一个文件描述符(整数),该数字用来区分一个程序打开的多个文件。文件描述符的作用域就是当前进程。

    1.Linux七种文件类型

    普通文件类型
    Linux中最多的一种文件类型, 包括 纯文本文件(ASCII);二进制文件(binary);数据格式的文件(data);各种压缩文件.第一个属性为 [-]

    目录文件
    就是目录, 能用cd 命令进入的。第一个属性为 [d],例如 [drwxrwxrwx]

    块设备文件
    块设备文件 : 就是存储数据以供系统存取的接口设备,简单而言就是硬盘。例如一号硬盘的代码是 /dev/hda1等文件。第一个属性为 [b]

    字符设备
    字符设备文件:即串行端口的接口设备,例如键盘、鼠标等等。第一个属性为 [c]

    套接字文件
    这类文件通常用在网络数据连接。可以启动一个程序来监听客户端的要求,客户端就可以通过套接字来进行数据通信。第一个属性为 [s],最常在 /var/run目录中看到这种文件类型

    管道文件
    FIFO也是一种特殊的文件类型,它主要的目的是,解决多个程序同时存取一个文件所造成的错误。FIFO是first-in-first-out(先进先出)的缩写。第一个属性为 [p]

    链接文件
    类似Windows下面的快捷方式。第一个属性为 [l],例如 [lrwxrwxrwx]

    Linux中文件扩展名
    windows里通过扩展名来区分文件类型的。linux里文件扩展名和文件类型没有关系。但为了容易区分和兼容用户使用windows的习惯,还是会用扩展名来表示文件类型。举例如下:
    ● 源码.tar、.tar.gz、.tgz、.zip、.tar.bz表示压缩文件,创建命令一般为tar,gzip,zip等。
    ● .sh表示shell脚本文件,通过shell语言开发的程序。
    ● .pl表示perl语言文件,通过perl语言开发的程序。
    ● .py表示python语言文件,通过python语言开发的程序。
    ● .html、.htm、.php、.jsp、.do表示网页语言的文件。
    ● .conf表示系统服务的配置文件。
    ● .rpm表示rpm安装包文件。
     

    2.Linux常用文件IO接口(API)

    2.1 open :open and possibly create a file·

    1. #include
    2. #include
    3. #include
    4. int open(const char *pathname, int flags);
    5. int open(const char *pathname, int flags, mode_t mode);

    返回值:open函数的返回值如果操作成功,它将返回一个文件描述符,如果操作失败,它将返回-1。

    pathname:在open函数中第一个参数pathname是指向想要打开的文件路径名,或者文件名。我们需要注意的是,这个路径名是绝对路径名。文件名则是在当前路径下的。

    flags:flags参数表示打开文件所采用的操作,我们需要注意的是:必须指定以下三个常量的一种,且只允许指定一个

    • O_RDONLY:只读模式
    • O_WRONLY:只写模式
    • O_RDWR:可读可写

    以下的常量是选用的,这些选项是用来和上面的必选项进行按位或起来作为flags参数。

    • O_APPEND 表示追加,如果原来文件里面有内容,则这次写入会写在文件的最末尾。
    • O_CREAT 表示如果指定文件不存在,则创建这个文件
    • O_EXCL 表示如果要创建的文件已存在,则出错,同时返回 -1,并且修改 errno 的值。
    • O_TRUNC 表示截断,如果文件存在,并且以只写、读写方式打开,则将其长度截断为0。
    • O_NOCTTY 如果路径名指向终端设备,不要把这个设备用作控制终端。
    • O_NONBLOCK 如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继 I/O设置为非阻塞模式(nonblocking mode)

    以下三个常量同样是选用的,它们用于同步输入输出

    • O_DSYNC 等待物理 I/O 结束后再 write。在不影响读取新写入的数据的前提下,不等待文件属性更新。
    • O_RSYNC read 等待所有写入同一区域的写操作完成后再进行
    • O_SYNC 等待物理 I/O 结束后再 write,包括更新文件属性的 I/Omode:

    mode:表示设置文件访问权限的初始值,和用户掩码umask有关,比如0644表示-rw-r–r–,也可以用S_IRUSR、S_IWUSR等宏定义按位或起来表示,详见open(2)的Man Page。

    文件权限由open的mode参数和当前进程的umask掩码共同决定。

    第三个参数是在第二个参数中有O_CREAT时才作用,如果没有,则第三个参数可以忽略

    mode 的 具体 参数:

    S_IRWXU
    00700 允许 文件 的 属主 读 , 写 和 执行 文件
    S_IRUSR (S_IREAD)
    00400 允许 文件 的 属主 读 文件
    S_IWUSR (S_IWRITE)
    00200 允许 文件 的 属主 写 文件
    S_IXUSR (S_IEXEC)
    00100 允许 文件 的 属主 执行 文件
    S_IRWXG
    00070 允许 文件 所在 的 分组 读 , 写 和 执行 文件
    S_IRGRP
    00040 允许 文件 所在 的 分组 读 文件
    S_IWGRP
    00020 允许 文件 所在 的 分组 写 文件
    S_IXGRP
    00010 允许 文件 所在 的 分组 执行 文件
    S_IRWXO
    00007 允许 其他 用户 读 , 写 和 执行 文件
    S_IROTH
    00004 允许 其他 用户 读 文件
    S_IWOTH
    00002 允许 其他 用户 写 文件
    S_IXOTH
    00001 允许 其他 用户 执行 文件

    2.2 read : read from a file descriptor

    1. #include
    2. ssize_t read(int fd, void *buf, size_t count);

    返回值:读到文件末尾返回0;正常则返回读到的字节数;当有错误发生时则返回-1,错误代码存入errno中。

    fd:文件描述符,buf:读取数据存放位置,count:需要读取的数据大小

    2.3 write : write to a file descriptor

    1. #include
    2. ssize_t write(int fd, const void *buf, size_t count);

    返回值:如果顺利write()会返回实际写入的字节数(len)。当有错误发生时则返回-1,错误代码存入errno中。

    fd:文件描述符,buf:待写入数据,count:待写入数据大小

    2.4 close :close a file descriptor

    1. #include
    2. int close(int fd);

    2.5 lseek: reposition read/write file offset

    1. #include
    2. #include
    3. off_t lseek(int fd, off_t offset, int whence);

    (1)文件指针:当我们要对一个文件进行读写时,一定需要先打开文件,所以我们读写的所有文件都是动态文件。动态文件在内存中的形态就是文件流的形式。

    (2)文件流很长,里面有很多个字节。那我们当前正在操作的是哪个位置?GUI模式下的软件用光标来识别这个当前正在操作的位置,这个给人看的

    (3)在动态文件中,通过文件指针来表征这个正在操作的位置。所谓文件指针,就是我们文件管理表这个结构体里面的一个指针。所以文件指针其实是vnode中的一个元素。这个指针表示当前我们正在操作文件流的那个位置。这个指针不能被直接访问,Linux系统用lseek函数来访问这个文件指针。

    (4)当我们打开一个空文件时,默认情况下文件指针指向文件流的开始。所以这个时候去write时写入就是从文件开头开始的。write和read函数本身自带移动文件指针的功能,所以当我write了n个字节后,文件指针会自动向后移动n位。如果需要人为的随意更改文件指针,那就只能通过lseek函数了

    (5)read和write函数都是从当前文件指针处开始操作的,所以当我们用lseek显示的将文件指针移动后,那么再去read/write时就是从移动过后的位置开始的。

    返回值:成功返回当前位置到开始的长度,失败返回-1并设置errno

    fd:文件描述符,offset:偏移量

    whence:位置
    SEEK_SET:The offset is set to offset bytes. offset为0时表示文件开始位置
    SEEK_CUR:The offset is set to its current location plus offset bytes. offset为0时表示当前位置
    SEEK_END:The offset is set to the size of the file plus offset bytes. offset为0时表示结尾位置
     

    lseek返回的是偏移量,所以如下其实就是返回文件长度

    ret = lseek(fd,0,SEEK_END);

    2.6 ioctl:control device

    1. #include
    2. int ioctl(int fd, unsigned long request, ...);

    函数说明:

    fd:表示文件描述符;

    request:表示与驱动程序交互的命令, 用不同的命令控制驱动程序输出我们
    需要的数据;

    … :表示可变参数 arg, 根据 request 命令, 设备驱动程序返回输出的数据。

    返回值: 打开成功返回文件描述符,失败将返回-1。

    ioctl 的作用非常强大、灵活。不同的驱动程序内部会实现不同的 ioctl,APP 可以使用各种 ioctl 跟驱动程序交互:可以传数据给驱动程序,也可以从驱动程序中读出数据。
     

    3. 应用层io进入内核发生了什么

    这里不作深入讲解,只需知道基本内容:

    Linux应用程序调用的open()、read()等函数时,触发内部(32位cpu:swi,64位cpu:svc)指令,触发异常(在触发异常时还会传入不同的参数给内核,根据参数来分辨),然后调用内核中对应的sys_open()、sys_read()等函数。

    会分辨目标文件到底是七种文件中的哪一种,例如普通文件,就去调用文件系统对应的驱动代码,读取块设备上的文件。例如目标文件是我们想驱动的字符设备文件,就会去调用们自己写的字符设备驱动程序中的read(),write()等。

    4.小练习;自己写个copy复制文件

    挺简单的,直接上代码了

    1. #include <sys/types.h>
    2. #include <sys/stat.h>
    3. #include <fcntl.h>
    4. #include <unistd.h>
    5. int main(int argc,char **argv)
    6. {
    7. int src;
    8. int dst;
    9. int buf[1024];
    10. int read_len,write_len;
    11. if(argc!=3)
    12. {
    13. printf("Input Error\n");
    14. return -1;
    15. }
    16. src=open(argv[1],O_RDONLY);
    17. if(src<=0)
    18. {
    19. printf("open %s error\n",argv[1]);
    20. return -1;
    21. }
    22. dst=open(argv[2],O_RDWR|O_CREAT,S_IRWXU);
    23. if(dst<=0)
    24. {
    25. printf("open %s error\n",argv[2]);
    26. return -1;
    27. }
    28. while(1)
    29. {
    30. read_len=read(src,buf,1024);
    31. if(read_len<0)
    32. {
    33. printf("read %s error\n",argv[1]);
    34. return -1;
    35. }
    36. if(read_len>0)
    37. {
    38. write_len=write(dst,buf,read_len);
    39. if(write_len<0)
    40. {
    41. printf("read %s error\n",argv[1]);
    42. return -1;
    43. }
    44. }
    45. else
    46. break;
    47. }
    48. close(src);
    49. close(dst);
    50. return 0;
    51. }

    把源代码编译

    gcc -o copy copy.c

    使用vi命令随便写点东西

    vi test1

     使用我们自己的copy命令

    ./copy test1 test2

    成功。 

  • 相关阅读:
    《Docker极简教程》--Docker镜像--Docker镜像的管理
    Laravel5使用box/spout扩展,大文件导出CSV文件
    Spark SQL 实现分层抽样和分层随机抽样
    第1章Python语言基础-1.1变量与表达式(二)
    MySQL8.0忘记密码
    ds项目管理
    设计实例13-跨时钟域
    CSP-J1 CSP-S1 信奥 第1轮 初赛 数据分析 成绩及分数线汇总
    JMeter笔记3 | JMeter安装和环境说明
    labview运行速度太慢
  • 原文地址:https://blog.csdn.net/freestep96/article/details/126645277