【嵌入式Linux】嵌入式Linux应用开发基础知识之文件IO

  • Post author:
  • Post category:linux




前言

韦东山嵌入式Linux应用开发基础知识学习笔记

文章中大多内容来自韦东山老师的文档,还有部分个人根据自己需求补充的内容


视频教程地址:

https://www.bilibili.com/video/BV1kk4y117Tu



1、文件从哪来

在这里插入图片描述



▲文件从哪来?



2、文件如何访问



2.1、通用IO模型open/read/write/lseek/close



2.1.1、通用IO模型使用示例


copy.c

:实现使用

./copy <file_old> <file_new>

来复制旧文件到新文件

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

/*
 * ./copy 1.txt 2.txt
 * argc    = 3
 * argv[0] = "./copy"
 * argv[1] = "1.txt"
 * argv[2] = "2.txt"
 */
int main(int argc, char **argv)
{
	int fd_old, fd_new;
	char buf[1024];
	int len;
	
	/* 1. 判断参数 */
	if (argc != 3) 
	{
		printf("Usage: %s <old-file> <new-file>\n", argv[0]);
		return -1;
	}

	/* 2. 打开老文件 */
	fd_old = open(argv[1], O_RDONLY);//只读方式打开文件
	if (fd_old == -1)//如果打开失败
	{
		printf("can not open file %s\n", argv[1]);
		return -1;
	}

	/* 3. 创建新文件 */
	/*	
		flags: 只写 创建 如果文件存在则清空文件内容 
		mode: User-read User-write Group-read Group-write Other-read Other-write 
	*/
	fd_new = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
	if (fd_new == -1)//如果打开失败
	{
		printf("can not creat file %s\n", argv[2]);
		return -1;
	}

	/* 4. 循环: 读老文件-写新文件 */
	//将文件中1024个字节数据存储到buf,函数会返回取出字节数
	while ((len = read(fd_old, buf, 1024)) > 0)
	{
		if (write(fd_new, buf, len) != len)//将buf中的数据写入fd_new文件
		{
			printf("can not write %s\n", argv[2]);
			return -1;
		}
	}

	/* 5. 关闭文件 */
	close(fd_old);
	close(fd_new);
	
	return 0;
}

在这里插入图片描述



▲copy使用实例



2.1.2、open/read/write/lseek/close函数解读

以下内容大多来源于《Linux/UNIX系统编程手册》第四章文件I/O:通用的I/O模型


  • 函数原型



    int open(const char *pathname, int flags, ... /*mode_t mode*/)

  • 返回值



    Returns file descriptor on sucess,or -1 on error

    返回值解析见《Linux/UNIX系统编程手册》

  • 参数解析




    pathname

    :要打开的文件由参数 pathname 来标识,如果 pathname 是一符号链接,会对其进行解引用


    flags

    :位掩码,用于指定文件的访问模式

    文件的访问模式:

    在这里插入图片描述



▲文件的访问模式

open()系统调用的 flags 参数值介绍

在这里插入图片描述

在这里插入图片描述



▲open()调用中的 flags 参数

参数可按功能分类分为三类:

  • 文件访问模式标志: O_RDONLY、O_WRONLY 和 O_RDWR 标志均在此列,调用 open()时,上述三者在 flags 参数中不能同时使用,只能指定其中一种。调用fcntl()的 F_GETFL 操作能够检索文件的访问模式
  • 文件创建标志:这些标志在表中位于第二部分,其控制范围不拘于 open()调用行为的方方面面,还涉及后续 I/O 操作的各个选项。这些标志不能检索,也无法修改
  • 已打开文件的状态标志:这些标志是表中的剩余部分,使用 fcntl()的 F_GETFL 和 F_SETFL 操作可以分别检索和修改此类标志。有时干脆将其称之为文件状态标志


O_ASYNC



当对于 open()调用所返回的文件描述符可以实施 I/O 操作时,系统会产生一个信号通知进程。这一特性,也被称为信号驱动 I/O,仅对特定类型的文件有效,诸如终端、FIFOS 及 socket




O_EXCL

:_EXCL 此标志与 O_CREAT 标志结合使用表明如果文件已经存在,则不会打开文件,且 open()调用失败,并返回错误,错误号 errno 为 EEXIST。换言之,

此标志确保了调用者(open( )的调用进程)就是创建文件的进程。



O_CLOEXEC

:为新(创建)的文件描述符启用 close-on-flag 标志(FD_CLOEXEC),使用 O_CLOEXEC 标志(打开文件),可以免去程序执行 fcntl()的 F_GETFD和 F_SETFD 操作来设置 close-on-exec 标志的额外工作。在多线程程序中执行 fcntl() 的 F_GETFD和 F_SETFD 操作有可能导致竞争状态,而使用 O_CLOEXEC 标志则能够避免这一点


O_CREAT

:如果文件不存在,将创建一个新的空文件。即使文件以只读方式打开,此标志依然有效。如果在 open()调用中指定 O_CREAT 标志,那么还需要提供 mode 参数,否则,会将新文件的权限设置为栈中的某个随机值


O_DIRECT

:无系统缓冲的文件 I/O 操作。为使 O_DIRECT 标志的常量定义在<fcntl.h>中有效,必须定义_GNU_SOURCE 功能测试宏。


O_DIRECTORY

:如果 pathname 参数并非目录,将返回错误(错误号 errno 为 ENOTDIR)。这一标志是专为实现 opendir()函数(18.8 节)而设计的扩展标志。为使 O_DIRECTORY 标志的常量定义在<fcntl.h>中有效,必须定义_GNU_SOURCE 功能测试宏。


O_DSYNC

:根据同步 I/O 数据完整性的完成要求来执行文件写操作


O_LARGEFILE

:支持以大文件方式打开文件。在 32 位操作系统中使用此标志,以支持大文件操作


O_NOATIME

:在读文件时,不更新文件的最近访问时间此标志是 Linux 特有的非标准扩展。要从<fcntl.h>中启用此标志,必须定义_GNU_SOURCE 功能测试宏。O_NOATIME 标志的设计旨在为索引和备份程序服务。该标志的使用能够显著减少磁盘的活动量,省却了既要读取文件内容,又要更新文件 i-node 结构中最近访问时间的繁琐,进而节省了磁头在磁盘上的反复寻道时间


O_NOCTTY

:如果正在打开的文件属于终端设备,O_NOCTTY 标志防止其成为控制终端。如果正在打开的文件不是终端设备,则此标志无效


O_NOFOLLOW

:通常,如果 pathname 参数是符号链接,open()函数将对 pathname 参数进行解引用。一旦

在 open()函数中指定了 O_NOFOLLOW 标志,且 pathname 参数属于符号链接,则 open()函数将返回失败(错误号 errno 为 ELOOP)。此标志在特权程序中极为有用,能够确保 open()函数不对符号链接进行解引用。为使 O_NOFOLLOW 标志在<fcntl.h>中有效,必须定义_GNU_SOURCE功能测试宏


O_TRUNC



如果文件已经存在且为普通文件,那么将清空文件内容,将其长度置 0。在 Linux 下使用此标志,无论以读、写方式打开文件,都可清空文件内容(在这两种情况下,都必须拥有对文件的写权限)。



mode

:当调用 open()创建新文件时,位掩码参数 mode 指定了文件的访问权限。(SUSv3 规定,mode的数据类型mode_t 属于整数类型。)如果 open()并未指定 O_CREAT 标志,则可以省略 mode 参数。

需要注意 mode 参数可以指定为数字(通常为八进制数),更为可取的做法是对 0 个或多个位掩码常量进行逻辑或(|)操作。

在这里插入图片描述

在这里插入图片描述



▲mode中用来表示文件权限位的常量


  • 函数原型



    ssize_t read(int fd,void *buffer,size_t count)

  • 返回值



    Returns number of bytes read,0 on EOF,or -1 on error

  • 函数功能说明




    系统调用不会分配内存缓冲区用以返回信息给调用者。所以,必须预先分配大小合适的缓冲区并将缓冲区指针传递给系统调用

    。与此相反,有些库函数却会分配内存缓冲区用以返回信息给调用者。

    如果 read()调用成功,将返回实际读取的字节数,如果遇到文件结束(EOF)则返回 0,如果出现错误则返回-1。ssize_t 数据类型属于有符号的整数类型,用来存放(读取的)字节数或-1(表示错误)


    一次 read()调用所读取的字节数可以小于请求的字节数



    read()应用于其他文件类型时,比如管道、FIFO、socket 或者终端,在不同环境下也会出现 read()调用读取的字节数小于请求字节数的情况。例如,默认情况下从终端读取字符,一遇到换行符(\n),read()调用就会结束


    read()能够从文件中读取任意序列的字节,但是读取到的字节是原封不动的存在缓冲变量中的,所以再输出前需要将其末尾添加一个结束符号

    在这里插入图片描述



▲读取文件后printf操作示例


  • 函数原型



    ssize_t write(int fd,void *buffer,size_t count)


  • 返回值



    Returns number of bytes wriiten,0 on EOF,or -1 on error


  • 函数功能说明



    write()调用的参数含义与 read()调用相类似。buffer 参数为要写入文件中数据的内存地址,count参数为欲从 buffer 写入文件的数据字节数,fd 参数为一文件描述符,指代数据要写入的文件。

    如果 write()调用成功,将返回实际写入文件的字节数,该返回值可能小于 count 参数值。这被称为“部分写”。对磁盘文件来说,造成“部分写”的原因可能是由于磁盘已满,或是因为进程资源对文件大小的限制。


    对磁盘文件执行 I/O 操作时,write()调用成功并不能保证数据已经写入磁盘。因为为了减少磁盘活动量和加快 write()系统调用,内核会缓存磁盘的 I/O 操作。


  • 函数原型



    int close(int fd)


  • 返回值



    Returns 0 on success,or -1 on error


  • 函数功能说明

    :显式关闭不再需要的文件描述符往往是良好的编程习惯,会使代码在后续修改时更具可读性,也更可靠。进而言之,文件描述符属于有限资源,因此文件描述符关闭失败可能会导致一个进程将文件描述符资源消耗殆尽。在编写需要长期运行并处理大量文件的程序时,比如 shell 或者网络服务器软件,需要特别加以关注。


  • 函数原型



    off_t lseek(int fd, off_t offset, int whence)


  • 返回值



    Returns new file offset if successful,or -1 on error


  • 函数功能说明

    :见《Linux/Unix系统编程手册》



2.2、不是通用的函数:ioctl/mmap



2.2.1、ioctl/mmap使用示例


copy.c

:实现使用

./copy_mmap <file_old> <file_new>

来复制旧文件到新文件

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

/*
 * ./copy 1.txt 2.txt
 * argc    = 3
 * argv[0] = "./copy"
 * argv[1] = "1.txt"
 * argv[2] = "2.txt"
 */
int main(int argc, char **argv)
{
	int fd_old, fd_new;
	struct stat stat;
	char *buf;
	
	/* 1. 判断参数 */
	if (argc != 3) 
	{
		printf("Usage: %s <old-file> <new-file>\n", argv[0]);
		return -1;
	}

	/* 2. 打开老文件 */
	fd_old = open(argv[1], O_RDONLY);
	if (fd_old == -1)
	{
		printf("can not open file %s\n", argv[1]);
		return -1;
	}

	/* 3. 确定老文件的大小 */
	if (fstat(fd_old, &stat) == -1)
	{
		printf("can not get stat of file %s\n", argv[1]);
		return -1;
	}

	/* 4. 映射老文件 */
	//创建一个区域内容可读取的共享映射
	buf = mmap(NULL, stat.st_size, PROT_READ, MAP_SHARED, fd_old, 0);
	if (buf == MAP_FAILED)
	{
		printf("can not mmap file %s\n", argv[1]);
		return -1;
	}

	/* 5. 创建新文件 */
	fd_new = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
	if (fd_new == -1)
	{
		printf("can not creat file %s\n", argv[2]);
		return -1;
	}

	/* 6. 写新文件 */
	if (write(fd_new, buf, stat.st_size) != stat.st_size)
	{
		printf("can not write %s\n", argv[2]);
		return -1;
	}

	/* 5. 关闭文件 */
	close(fd_old);
	close(fd_new);
	
	return 0;
}

在这里插入图片描述



▲copy_mmap使用实例



2.2.2、ioctl/mmap函数解读

参考《Linux/Unix系统编程手册》第49.2章


  • 函数原型



    int ioctl(int fd, int request, ... /*argp*/)


  • 返回值



    Value returned on success depends on request,or -1 on error


  • 函数功能说明



    fd 参数为某个设备或文件已打开的文件描述符,request 参数指定了将在 fd 上执行的控制操作。具体设备的头文件定义了可传递给 request 参数的常量。

    ioctl()调用的第三个参数采用了标准 C 语言的省略符号(…)来表示(称之为 argp),可以是任意数据类型。ioctl()根据 request 的参数值来确定 argp 所期望的类型。通常情况下,argp是指向整数或结构的指针,有些情况下,不需要使用 argp。


  • 函数原型



    void *mmap(void *addr, sizer_t length, int prot, int flags, int fd, off_t offset)


  • 返回值



    Returns starting address of mapping on success, or MAP_FAILED on error


  • 函数功能说明




    addr

    参数指定了映射被放置的虚拟地址。如果将 addr 指定为 NULL,那么内核会为映射选择一个合适的地址。这是创建映射的首选做法。或者在 addr 中指定一个非 NULL 值时,内核会在选择将映射放置在何处时将这个参数值作为一个提示信息来处理。在实践中,内核至少会将指定的地址舍入到最近的一个分页边界处。

    成功时 mmap()会返回新映射的起始地址。发生错误时 mmap()会返回 MAP_FAILED。


    prot

    参数是一个位掩码,它指定了施加于映射之上的保护信息,其取值要么是PROT_NONE,要么是表中列出的其他三个标记的组合(取 OR)。

    在这里插入图片描述



▲prot参数候选项


flags

参数是一个控制映射操作各个方面的选项的位掩码。这个掩码必须只包含下列值中一个。


MAP_PRIVATE


创建一个私有映射。区域中内容上所发生的变更对使用同一映射的其他进程是不可见的,对于文件映射来讲,所发生的变更将不会反应在底层文件上。


MAP_SHARED


创建一个共享映射。区域中内容上所发生的变更对使用 MAP_SHARED 特性映射同一区域的进程是可见的,对于文件映射来讲,所发生的变更将直接反应在底层文件上。对文件的更新将无法确保立即生效,具体可参加 49.5 节中对 msync()系统调用的介绍。

除了 MAP_PRIVATE 和 MAP_SHARED 之外,在 flags 中还可以有选择地对其他标记取OR。在 49.6 和 49.10 节中将会对这些标记进行介绍。

剩余的参数

fd



offset

是用于文件映射的(匿名映射将忽略它们)。fd 参数是一个标识被映射的文件的文件描述符。offset 参数指定了映射在文件中的起点,它必须是系统分页大小的倍数。要映射整个文件就需要将 offset 指定为 0 并且将 length 指定为文件大小。



3、内核接口

在这里插入图片描述



▲系统调用函数怎么进入内核

在这里插入图片描述



▲内核的sys_open、sys_read 会做什么



版权声明:本文为weixin_43444989原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。