当前位置:   article > 正文

Linux C 应用编程学习笔记 ——(3)深入探究文件 I/O_o_trunc

o_trunc

《【正点原子】I.MX6U嵌入式Linux C应用编程指南》学习笔记

Linux 系统如何管理文件

静态文件与 inode

文件在没有打开时,都是存放在磁盘等存储设备中并且以一种固定的形式存放,我们把它们称为静态文件。我们在程序中调用 open() 函数是如何找到文件所在的存储位置呢,这里就要提到 inode 的概念了。

inode 是 UNIX 操作系统中的一种数据结构,其本质是结构体,它包含了与文件系统中各个文件相关的一些重要信息。在 UNIX 中创建文件系统时,同时将会创建大量的 inode 。通常,文件系统磁盘空间中大约百分之一空间分配给了 inode 表。

下面的定义仅给出了 inode 中所包含的、UNIX 用户经常使用的一些重要信息:
● inode 编号
● 用来识别文件类型,以及用于 stat C 函数的模式信息
● 文件的链接数目
● 属主的ID (UID)
● 属主的组 ID (GID)
● 文件的大小
● 文件所使用的磁盘块的实际数目
● 最近一次修改的时间
● 最近一次访问的时间
● 最近一次更改的时间

——百度百科

打开一个文件,系统内部将会进行以下三步:

  1. 系统找到文件对应的 inode 编号;
  2. 根据 inode 编号在 inode table 中查找相应的 inode 结构体;
  3. 根据 inode 结构体确定文件数据所在位置(块),然后读取文件数据。

返回错误处理与 errno

平时我们编写代码时,判断函数执行失败后,会使用 return 退出程序,但是很难确定具体出错的原因(一般出错时我们都返回 -1)。Linux 系统下对常见错误编了号,分别对应不同的错误类型,这些错误编号赋值给了 errno 变量。

errno 是记录系统的最后一次错误代码。代码是一个int型的值,在errno.h中定义。查看错误代码errno是调试程序的一个重要方法。当linux C api函数发生异常时,一般会将errno变量(需include errno.h)赋一个整数值,不同的值表示不同的含义,可以通过查看该值推测出错的原因。在实际编程中用这一招解决了不少原本看来莫名其妙的问题。

——百度百科

下面举个简单的例子,当前目录没有 “test_file” 这个文件,所以会运行 if 内的语句。

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

int main()
{
	int fd;

	fd = open("test_file", O_RDWR);

	if(fd < 0)
	{
		//printf("test_file open failed.\n");
		printf("%d\n", errno);
		return -1;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

这个程序的运行结果为 2(前提是 test_file 文件不存在)。错误码 2 对应的错误类型为 “文件或目录不存在”,错误码对应的含义可以在 /usr/include/asm-generic/errno.herrno-base.h 中查看(ubuntu16.04)。

在这里插入图片描述

strerror()

前面提到的 errno 只是一个数字,如果每次都要查看错误号对应的含义,未免太过麻烦。这里介绍一个 C 库函数 strerror(),该函数可以直接获取错误码对应的错误信息(字符串形式),该函数原型如下:

#include <string.h>
char *strerror(int errnum);
  • 1
  • 2

下面是一个简单的测试代码:

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


int main()
{
	int fd;
	
	fd = open("test_file", O_RDWR);
	if(fd < 0)
	{
		//printf("test_file open failed.\n");
		printf("%s\n", strerror(errno));
		return -1;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

运行结果:

在这里插入图片描述

perror()

除了 strerror() 函数,perror() 函数也能用来查看错误信息,而且它比 strerror() 更加方便,strerror() 还需要传入 errno 且只能获取字符串,而 perror() 可以直接获取错误信息且将其打印出来。函数原型如下:

#include <stdio.h>
void perror(const char *s);
  • 1
  • 2

参数 s 是错误的自定义提示信息。

简单的测试代码:

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

int main()
{
	int fd;

	fd = open("test_file", O_RDWR);
	if(fd < 0)
	{
		//printf("test_file open failed.\n");
		perror("");
		perror("错误提示");
		return -1;
	}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

运行结果:

在这里插入图片描述

如果 perror() 的参数不为空字符串,那么该函数会自动在提示信息后面加上冒号和空格。

exit()、_exit()、_Exit()

在程序遇到错误时,我们有时会使用 return 将程序终止,一般情况下,正常退出使用 return 0,错误返回 return -1。在 Linux 中,进程正常退出除了可以使用 return 外,还能用 exit()、_exit() 及 _Exit()。

_exit() 和 _Exit()

这两个函数是等价的,原型分别是:

#include <unistd.h>
void _exit(int status);
  • 1
  • 2
#include <stdlib.h>
void _Exit(int status);
  • 1
  • 2

这两个函数会结束当前进程,并且清除其使用的内存空间,关闭进程的所有文件描述符。

这里拿 _exit() 来做一个简单测试:

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

int main()
{
	int fd;

	fd = open("test_file", O_RDWR);
	if(fd < 0)
	{
		//printf("test_file open failed.\n");
		//perror("");
		perror("错误提示");
		_exit(-1);
	}
	close(fd);
	_exit(0);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

运行结果,$? 为上一次函数调用的返回值。

在这里插入图片描述

exit()

exit() 是一个标准 C 库函数,而 _exit() 和 _Exit() 是系统调用,执行 exit() 时,最后也会执行 _exit((),只不过 exit() 还会多做一些清理工作。exit() 原型如下:

#include <stdlib.h>
void exit(int status);
  • 1
  • 2

该函数用法和 _exit() 相同,但原文推荐我们用 exit()。

空洞文件

在UNIX文件操作中,文件位移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将延长该文件,并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被设为 0。

如果 offset 比文件的当前长度更大,下一个写操作就会把文件“撑大(extend)”。这就是所谓的在文件里创造“空洞(hole)”。没有被实际写入文件的所有字节由重复的 0 表示。空洞是否占用硬盘空间是由文件系统(file system)决定的。

主要特点

  • 用ls查看的文件大小是将空洞算在内的。
  • cp命令拷贝的文件,空洞部分不拷贝,所以生成的同样文件占用磁盘空间小
  • 用read读取空洞部分读出的数据是0,所以如果用read和write拷贝一个有空洞的文件,那么最终得到的文件没有了空洞,空洞部分都被0给填充了,文件占用的磁盘空间就大了。不过文件大小不变。
    空洞文件作用很大,例如迅雷下载文件,在未下载完成时就已经占据了全部文件大小的空间,这时候就是空洞文件。下载时如果没有空洞文件,多线程下载时文件就都只能从一个地方写入,这就不是多线程了。如果有了空洞文件,可以从不同的地址写入,就完成了多线程的优势任务

——百度百科

O_APPEND 和 O_TRUNC 标志

O_APPEND 和 O_TRUNC 是 oepn() 函数的两个标志。

O_TRUNC 标志

使用 O_TRUNC 这个标志,文件打开时就会把文件原来的数据丢弃。下面是一个简单的测试代码,(文件 test_file 原来是有数据的)

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

int main()
{
	int fd;

	fd = open("test_file", O_RDWR|O_TRUNC);
	if(fd < 0)
	{
		perror("错误提示");
		_exit(-1);
	}
	close(fd);
	_exit(0);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

运行结果如下,原本 test_file 内有 2322 字节的数据,使用 O_TRUNC 标志打开文件后,文件数据被清空:

在这里插入图片描述
O_APPEND 标志

如果 oepn 函数使用了 O_APPEND 标志,当使用 write() 函数对文件进行写操作时,文件指针会先被移动到文件末尾。下面是测试代码:

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

char buff[] = "ccc";

int main()
{
	int fd;

	fd = open("test_file", O_RDWR|O_APPEND);
	if(fd < 0)
	{
		perror("错误提示");
		_exit(-1);
	}

	/* 写入数据 */
	write(fd, buff, sizeof(buff));

	close(fd);
	_exit(0);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

测试结果如下,运行程序,“ccc” 被追加写入到了 test_file 中,但不知为什么总是要隔一个回车(难道文件尾带一个换行?也可能是因为 echo)

在这里插入图片描述

小提示:

  • O_APPEND 标志并不会影响读文件(read()),即读文件时依然是从文件头开始读。
  • 使用了 O_APPEND,如果在 write() 函数之前调用 lseek() 来移动文件指针,write() 写入数据依然会从文件末尾开始。

多次打开同一个文件

  • 一个进程内多次打开同一个文件,那么会得到多个不同的文件描述符,关闭文件时也需要将这些文件描述符都关闭;
  • 一个进程内多次打开同一个文件,在内存中并不会存在多份动态文件;
  • 一个进程内多次打开同一个文件,不同文件描述符所对应的读写偏移量是相互独立的;

复制文件描述符

在 Linux 系统中,可以使用 dup() 或 dup2() 这两个系统调用对文件描述符进行复制,复制得到的文件描述符和旧的文件描述符拥有相同的属性。

dup()

dup() 用于复制文件描述符,函数原型如下:

#include <unistd.h>
int dup(int oldfd);
  • 1
  • 2

参数 oldfd 为要复制的文件描述符。
函数运行成功时返回一个新的文件描述符,如果复制失败则返回 -1。

下面是一个简单的测试:

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

char buff1[] = "Hi, I am fd1\n";
char buff2[] = "Hi, I am fd2\n";

int main()
{
	int fd1, fd2;
	int cnt = 0;

	/* 打开文件 */
	fd1 = open("test_file", O_RDWR|O_TRUNC);
	if(fd1 < 0)
	{
		perror("错误提示");
		_exit(-1);
	}
	else
		printf("旧的文件描述符为%d\n", fd1);

	/* 复制文件描述符 */
	fd2 = dup(fd1);
	if(fd2 < 0)
	{
		perror("错误提示");
		_exit(-1);
	}
	else
		printf("新的文件描述符为%d\n", fd2);

	/* fd1 写入数据 */
	cnt = write(fd1, buff1, sizeof(buff1));
	printf("成功写入%d字节\n", cnt);

	/* fd2 写入数据 */
	cnt = write(fd2, buff2, sizeof(buff2));
	printf("成功写入%d字节\n", cnt);

	close(fd1);
	close(fd2);
	_exit(0);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

测试结果如下,两个文件描述符都能向 test_file 写入数据。

在这里插入图片描述

dup2()

dup() 和 dup2() 功能相同,惟一的区别是 dup() 返回的文件描述符由系统分配,而 dup2() 返回的文件描述符可以手动指定。下面是 dup2() 的原型:

#include <unistd.h>
int dup2(int oldfd, int newfd);
  • 1
  • 2

oldfd 为需要复制的文件描述符,newfd 为指定函数要返回的文件描述符。函数运行成功返回一个新的文件描述符,如果失败则返回 -1。

测试代码如下:

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

char buff1[] = "Hi, I am fd1\n";
char buff2[] = "Hi, I am fd2\n";

int main()
{
	int fd1, fd2;
	int cnt = 0;

	/* 打开文件 */
	fd1 = open("test_file", O_RDWR|O_TRUNC);
	if(fd1 < 0)
	{
		perror("错误提示");
		_exit(-1);
	}
	else
		printf("旧的文件描述符为%d\n", fd1);

	/* 复制文件描述符 */
	fd2 = dup2(fd1, 66);
	if(fd2 < 0)
	{
		perror("错误提示");
		_exit(-1);
	}
	else
		printf("新的文件描述符为%d\n", fd2);

	/* fd1 写入数据 */
	cnt = write(fd1, buff1, sizeof(buff1));
	printf("成功写入%d字节\n", cnt);

	/* fd2 写入数据 */
	cnt = write(fd2, buff2, sizeof(buff2));
	printf("成功写入%d字节\n", cnt);

	close(fd1);
	close(fd2);
	_exit(0);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

运行结果如下图所示,新的文件描述符为 dup2() 指定的 66。
在这里插入图片描述

文件共享

文件共享多用于多进程多线程编程环境中,它能减少文件读写时间、提升效率。

常见的三种文件共享的实现方式:

  1. 同一个进程多次调用 open() 函数打开同一个文件,各数据结构之间的关系如下图所示:
图片来源:《【正点原子】I.MX6U嵌入式Linux C应用编程指南》

在这里插入图片描述

  1. 不同进程中分别使用 open() 函数打开同一个文件,其数据结构关系如下图所示:
图片来源:《【正点原子】I.MX6U嵌入式Linux C应用编程指南》

在这里插入图片描述

  1. 同一个进程中通过 dup() 或 dup2() 函数对文件描述符进行复制,其数据结构关系如下图所示:
图片来源:《【正点原子】I.MX6U嵌入式Linux C应用编程指南》

在这里插入图片描述

原子操作与竞争冒险

竞争冒险简介

本小节给大家竞争冒险这个概念,如果学习过 Linux 驱动开发的读者对这些概念应该并不陌生,也就意味着竞争冒险不但存在于 Linux 应用层、也存在于 Linux 内核驱动层。

假设有两个独立的进程 A 和进程 B 都对同一个文件进行追加写操作(也就是在文件末尾写入数据),每一个进程都调用了 open 函数打开了该文件,但未使用 O_APPEND 标志,此时,每个进程都有它自己的进程控制块 PCB,有自己的文件表(意味着有自己独立的读写位置偏移量),但是共享同一个 inode 节点(也就是对应同一个文件)。假定此时进程 A 处于运行状态,B 未处于等待运行状态,进程 A 调用了 lseek 函数,它将进程 A 的该文件当前位置偏移量设置为 1500 字节处(假设这里是文件末尾),刚好此时进程 A 的时间片耗尽,然后内核切换到了进程 B,进程 B 执行 lseek 函数,也将其对该文件的当前位置偏移量设置为 1500 个字节处(文件末尾)。然后进程 B 调用 write 函数,写入了 100 个字节数据,那么此时在进程 B 中,该文件的当前位置偏移量已经移动到了 1600 字节处。B 进程时间片耗尽,内核又切换到了进程 A,使进程 A 恢复运行,当进程 A 调用 write 函数时,是从进程 A 的该文件当前位置偏移量(1500 字节处)开始写入,此时文件 1500 字节处已经不再是文件末尾了,如果还从 1500字节处写入就会覆盖进程 B 刚才写入到该文件中的数据。

以上给大家所描述的这样一种情形就属于竞争状态(也成为竞争冒险),操作共享资源的两个进程(或线程),其操作之后的所得到的结果往往是不可预期的,因为每个进程(或线程)去操作文件的顺序是不可预期的,即这些进程获得 CPU 使用权的先后顺序是不可预期的,完全由操作系统调配,这就是所谓的竞争状态。

——原文

原子操作

原子操作指的是不会被其他任务打断的一种操作。上面的例子中,在 lseek() 和 write() 执行的过程中可能被打断,所以这两个操作合起来不能称为原子操作。下面介绍一些原子操作举例:

  1. O_APPEND 实现原子操作:这方法可以直接解决上文提到的两个进程竞争问题。
  2. pread() 和 pwrite() :这两个函数可以实现原子操作,相对于 read() 和 write(),它们多了一个参数 offset,用于指定文件当前读写偏移量,且读写完成后不会影响原本的文件指针偏移量。他们的原型如下:
#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
  • 1
  • 2
  • 3
  1. 创建一个文件(O_EXCL):使用 O_EXCL 标志创建文件时,也是一个原子操作。假如自己在程序中判断文件是否存在(open() 失败代表文件不存在),然后再创建文件,那么当两个进程都执行这一操作时,就可能会出现竞争冒险问题(参考前面的例子)。

fcntl 和 ioctrl

fcntl() 函数

fcntl() 可以对一个已经打开的文件描述符进行一系列控制操作,如复制文件描述符、获取/设置文件描述符标志、获取/设置文件状态标志等。该函数的原型如下:

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ )
  • 1
  • 2
  • 3

该函数的参数是可变长度,搭配不同的 cmd 有不同的参数选项,具体用法可以使用 man 手册查看:

在这里插入图片描述
ioctl()

ioctl() 是一个文件 IO 操作的杂物箱,一般用于操作特殊文件或硬件外设,原型如下:

#include <sys/ioctl.h>
int ioctl(int fd, unsigned long request, ...);
  • 1
  • 2

request 参数没有统一值,可变参数 … 由 request 参数决定。

该函数后面再研究。

截断文件

系统调用 truncate() 和 ftruncate() 可以将普通文件截断为指定字节长度,它们的函数原型如下:

#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
  • 1
  • 2
  • 3
  • 4

这两个函数只有第一个参数不同,一个是以文件路径为参数,另一个是根据文件描述符匹配文件。调用成功返回 0,失败返回 -1。

下面是一个简单的测试程序:

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

char buff[] = "hello world, I am test_file";
char tmp[50];

int main()
{
	int fd;
	int cnt = 0;

	fd = open("test_file", O_RDWR|O_TRUNC);
	if(fd < 0)
	{
		perror("错误提示");
		_exit(-1);
	}

	/* 写入数据 */
	cnt = write(fd, buff, sizeof(buff));
	printf("成功写入%d字节\n", cnt);

	/* 读取文件内容 */
	memset(tmp, 0, sizeof(tmp));
	lseek(fd, 0, SEEK_SET);
	cnt = read(fd, tmp, sizeof(tmp));
	printf("成功读取%d字节\n", cnt);
	printf("截断前文件内容为:%s\n", tmp);

	/* 使用 truncate 将 test_file 截断为 11 字节 */ 
	truncate("test_file", 11);

	/* 读取文件内容 */
	memset(tmp, 0, sizeof(tmp));
	lseek(fd, 0, SEEK_SET);
	cnt = read(fd, tmp, sizeof(tmp));
	printf("成功读取%d字节\n", cnt);
	printf("第一次截断后文件内容为:%s\n", tmp);
	

	/* 使用 ftruncate 将 test_file 截断为 5 字节 */ 
	ftruncate(fd, 5);

	/* 读取文件内容 */
	memset(tmp, 0, sizeof(tmp));
	lseek(fd, 0, SEEK_SET);
	cnt = read(fd, tmp, sizeof(tmp));
	printf("成功读取%d字节\n", cnt);
	printf("第二次截断后文件内容为:%s\n", tmp);

	close(fd);
	_exit(0);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57

测试结果,

在这里插入图片描述

每次每次截断后,文件指针不会改变偏移位置,所以需要我们手动调整。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/186468
推荐阅读
相关标签
  

闽ICP备14008679号