一、linux中的文件IO

一、文件操作的一般步骤

open 一个文件 ——> 得到一个文件描述符 ——> 对该文件进行操作 ——> close文件

注:open一个文件时 linux 内核要做的事情:

  • 在进程中建立一个打开文件的数据结构,记录我们打开的文件
  • 内核在内存当中申请一段内存,将块设备当中静态文件,读取到这段内存当中来存放。
  • 我们以后的操作都是基于这个动态文件来进行的,只有最后 close 的时候才会同步到静态文件当中。

二、简单的一个文件读写的案例

1、man 手册的使用

man 1 xx 查linux shell命令,man 2 xxx 查API, man 3 xxx 查库函数

2、open函数

  • 格式
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

1、pathname(路径) :不仅有path,还有name , 说明 open 函数可以打开**其他文件夹下面的文件**2、flags :是一些特定的属性。
3、mode	 :只有当 flag 满足一定值的时候, 才使用 mode, 代表使用 O_CREAT 来创建文件时,指定的文件权限
4、返回值: fd ,表示打开**这个文件的文件描述符**。
 
 
flag 参数详解:
(1) 权限相关
The  argument  flags  must  include  one  of the following access modes:
 O_RDONLY, 	read-only,(只读)
 O_WRONLY,	write-only,(只写)
 O_RDWR.    read/write,(可读可写)
	 
注: linux 文件当中都有读写权限,flag 可以附带一定的权限说明的作用

2、 read 函数:

read:将文件的内容读到指定的内存区域当中。(void *buf)

ssize_t read(int fd, void *buf, size_t count);
参数: 
 fd: 指定操作哪一个文件 ———— 根据前面的open函数返回的值。
 void *buf : 应用程序提供的一段内存缓冲区。输出型参数 —— 这段内存是会被这个函数修改的
 count :我们要读去的字节数
返回值:
ssize_t : 是 typedef 重定义的一个类型,(其实就是int),返回值表示真正成功读取的字节数。如果是 -1 则表示阅读失败。

3、write 函数

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

分析过程:
const void *buf :本质是一个输入型参数 —— 在函数内部,并不会对 buf 进行修改
给 fd 这个文件写入 buf 中的 count 个字节。
返回值:
 ssize_t :真实写入的字节数
总结:
将 buf 里面的内存写入到 fd 这个文件当中,写入大小为 size_t count ,返回值为成功写入的大小。

4、exit、_exit、_Exit退出进程

分析我们之前的流程:
open 失败的时候,我们单纯的只打印了一个错误提示。
我们应该在失败之后, return -1 , 让他直接退出,不操作下面的程序。

如何退出程序:
(1)在 main 函数用 return 。一般的原则:程序正常终止 return 0 ,程序异常终止, return -1
因为在其他函数当中,return 仅仅单单代表返回值。不能退出

(2)使用 exit(库函数 man 3), _exit (系统API),或者 _Exit 其中一个。
小型项目

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

int main(int argc, char *argv[])
{
    
    
	int fd = -1;
	char read_buf[100] = {
    
    0};
	char write_buf[100] = "[email protected]";
	int read_size = -1;
	int write_size = -1;
	
	// open
	fd = open("a.txt",O_RDWR);
	if(fd < 0)
	{
    
    
	printf("文件打开失败 fd = %d\n",fd);
	_exit(-1);
	}
	else
	printf("文件打开成功 fd = %d\n",fd);
	

	
	//write
	write_size = write(fd,write_buf,strlen(write_buf));
	if(write_size < 0)
	{
    
    
	printf("write 失败\n");
	_exit(-1);
	}
	else
	printf("文件写入成功,大小为 %d\n",write_size);
	
	//read
	read_size = read(fd,read_buf,10);
	if(read_size < 0)
	{
    
    
	printf("read 失败\n");
	_exit(-1);
	}
	else
	printf("文件读取成功,大小为 %d\n",read_size);
	printf("内容为:%s \n",read_buf);
	
	
	close(fd);
	
	return 0 ;	
}

bug:写入成功,读取的时候没有得到自己想要的东西。
在这里插入图片描述
原因分析:在 write 的时候,已经将文件指针移动到了最后面, 所以我们 read 的时候就会读出东西。
解决办法:在write完,read前,利用 lseek 函数将文件指针移动到开头(第二节有细讲)

lseek(fd,0,SEEK_SET);

三、open 函数 flag 详解

  • (1) 权限相关
The  argument  flags  must  include  one  of the following access modes:

O_RDONLY, 	read-only,
O_WRONLY,	write-only,
O_RDWR.    read/write,
	 
注: linux 文件当中都有读写权限。我们 flag 可以附带一定的权限说明

fd = open("a.txt",O_RDONLY);
O_RDONLY : 当我们指定这个文件只读的时候, 后面就不可以对他进行写操作。
	/*
		文件打开成功, fd = 3
		实际读取了 5 字节 
		文件的内容是:[hello] 
		write 失败 
	*/
  • (2) 当我们打开的文件中本身有内容的时候
O_APPEND(附加)、O_TRUNC(取整): flag 这里用 或操作 来进行叠加。
fd = open("a.txt",O_RDONLY | O_APPEND);
可能结果1:新内容会替代原来的内容(原来的内容就不见了,丢了)
可能结果2:新内容添加在前面,原来的内容继续在后面
可能结果3:新内容附加在后面,原来的内容还在前面
可能结果4:不读不写的时候,原来的文件中的内容保持不变

O_TRUNC属性去打开文件时,如果这个文件中本来是有内容的,则原来的内容会被丢弃。这就对应上面的结果1
O_APPEND属性去打开文件时,如果这个文件中本来是有内容的,则新写入的内容会接续到原来内容的后面,对应结果3
O_APPEND、O_TRUNC 同时出现: O_TRUNC 起作用,O_APPEND被屏蔽。
默认不使用O_APPEND和O_TRUNC属性时就是结果4
  • (3) 打开不存在的文件时

O_CREAT、O_EXCL
思考:当我们去打开一个并不存在的文件时会怎样?
当我们open打开一个文件时如果这个文件名不存在则会 打开文件错误。

vi或者windows下的notepad++,都可以直接打开一个尚未存在的文件。
open的flag O_CREAT:就是为了应对这种打开一个并不存在的文件的。
O_CREAT就表示我们当前打开的文件并不存在,我们是要去创建并且打开它。

思考:当我们open使用了O_CREAT,但是文件已经存在的情况下会怎样?

结论:open中加入O_CREAT后,不管原来这个文件存在与否都能打开成功,
如果原来这个文件不存在则创建一个空的新文件。
如果原来这个文件存在则会重新创建这个文件,原来的内容会被消除掉(有点类似于先删除原来的文件再创建一个新的)。

这样可能带来一个问题?我们本来是想去创建一个新文件的,但是把文件名搞错了弄成了一个老文件名,结果老文件就被意外修改了。

我们希望的效果是:如果我CREAT要创建的是一个已经存在的名字的文件,则给我报错,不要去创建。

这个效果就要靠O_EXCL标志和O_CREAT标志来结合使用。当这连个标志一起的时候,则没有文件时创建文件,有这个文件时会报错提醒我们。

open函数在使用O_CREAT标志去创建文件时,可以使用第三个参数mode来指定要创建的文件的权限。
mode使用4个数字来指定权限的,其中后面三个很重要,对应我们要创建的这个文件的权限标志。
譬如一般创建一个可读可写不可执行的文件就用0666

  • (4)O_NONBLOCK 参数

阻塞和非阻塞式:

类比排队:
阻塞: 当我们在打饭排队的时候,我们如果要打饭,就得先排队,等轮到我们的时候,才能进行操作。(中间有个等待过程)
非阻塞:给我们特权,不用排队,直接操作。

阻塞式函数 : 这个函数可能需要排队,可能需要等一段时候才能执行,才能返回。
非阻塞式函数 :这个函数立马执行,然后立马返回。

阻塞和非阻塞是两种不同的设计思路,并没有好坏。
总的来说,阻塞式的 结果有保障 但是时间没保障; 非阻塞式的 时间有保障 但是结果没保障。

我们操作系统的 API 和 库函数,默认是阻塞式模式

引入生活中的例子:
假设12306这个软件是 阻塞式的,在他打开之前,会一直尝试着去进行联网操作,而不是显示页面,所以就会卡住。
我们可以配置成先显示,然后在后台进行联网操作。

应用范围:
我们这个阻塞和非阻塞只有应用在 设备文件 当中才有用, 设备文件:lcd ,串口,等等这些硬件器件。
对普通文件的操作,并没有任何意义。

  • (5)O_SYNC

无O_SYNC时write只是将内容写入底层缓冲区即可返回,
然后底层(操作系统中负责实现open、write这些操作的那些代码,也包含OS中读写硬盘等底层硬件的代码)在合适的时候会将buf中的内容一次性的同步到硬盘中。

这种设计是为了提升硬件操作的性能和销量,提升硬件寿命;因为硬盘也不是可以一直读写的,也有读写的上限。

但是有时候我们希望硬件不要等待,就是实时性比较强,直接将我们的内容写入硬盘中,这时候就可以用O_SYNC标志。

四、文件读写的细节

1、errno 全局变量 (可以通过 man 3 来进行查询) 和 perror函数

errno是由OS来维护的一个全局变量,任何OS内部函数都可以通过设置errno 来告诉上层调用者究竟刚才发生了一个什么错误。
errno就是error number,意思就是错误号码。
linux系统中对各种常见错误做了个编号,当函数执行错误时,函数会返回一个特定的errno编号来告诉我们这个函数到底哪里错了。
errno本身实质是一个int类型的数字,每个数字编号对应一种错误。当我们只看errno时只能得到一个错误编号数字
(譬如-37),不适应于人看。

总结:如果发生错误,操作系统会将错误写到 errno 这个全局变量当中

linux系统提供了一个函数perror(意思print error),
perror函数内部会读取errno并且将这个不好认的数字直接给转成对应的错误信息字符串,然后print打印出来。

perror(“文件打开错误”) :这个字符串会自动加到 error 环境变量之前。

注:perror 函数 必须用在可以设置 errno 的函数后面。怎么进行查看呢?
man 手册去查看返回过程:
在这里插入图片描述

2、read 和 write 函数中的 count 问题:

ssize_t  read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);

(1)count 和 返回值的问题:
count: 想要自己写的字节数。
返回值:真正实现的写的字节数。

(2)count 和阻塞和非阻塞的结合。
例如:我们想要读取 30 个字符, 结果有20个字符被阻塞住。

(3)不推荐一次读取所有文件。
推荐多次进行读取。
有时候我们写正式程序时,我们要读取或者写入的是一个很庞大的文件(譬如文件有2MB),我们不可能把count设置为210241024,而应该去把count设置为一个合适的数字(譬如2048、4096),然后通过多次读取来实现全部读完。

3、文件 IO 的效率比较低:太频繁的操作。

(1)文件IO就指的是我们当前在讲的open、close、write、read等API函数构成的一套用来读写文件的体系,这套体系可以很好的完成文件读写,但是效率并不是最高的。

APP <----> OS <------> 硬件

应对:标准IO 这些函数是通过对 API 封装而生成的
应用层 C语言库函数提供了一些用来做文件读写的函数列表。(fopen, fclose ,fwrite , fread )
作用: 主要是为了在应用层添加一个缓冲机制,

之前,是 APP ----> OS 层的 buf
现在,是 APP ----> APP 层的buf (这时候,标准IO库根据自己操作系统单次 write 的最佳count数选择最好的时机进行发送