Linux应用编程和网络编程(1)------- linux中的文件IO


一,文件操作的主要接口API

什么是文件IO
(1)IO就是input/output,输入/输出。文件IO的意思就是读写文件

1、什么是操作系统API
(1)API是一些函数,这些函数是由linux系统提供支持的,由应用层程序来使用。
(2)应用层程序通过调用API来调用操作系统中的各种功能,来干活。
(3)学习一个操作系统,其实就是学习使用这个操作系统的API。
(4)我们要使用linux系统来读写文件,手段就是学习linux系统API中和文件IO有关的几个。

在这里插入图片描述
2、文件操作的一般步骤

(1)在linux系统中要操作一个文件,一般是先open打开一个文件得到一个文件描述符,然后对文件进行读写操作(或其他操作),最后close关闭文件即可
(2)强调一点:我们对文件进行操作时,一定要先打开文件,打开成功后才能去操作(如果打开本身失败,后面就不用操作了);最后读写完成之后一定要close关闭文件,否则可能会造成文件损坏。
(3)文件平时是存在块设备中的文件系统中的,我们把这种文件叫静态文件。当我们去open打开一个文件时,linux内核做的操作包括:内核在进程中建立了一个打开文件的数据结构,记录下我们打开的这个文件;内核在内存中申请一段内存,并且将静态文件的内容从块设备中读取到内存中特定地址管理存放(叫动态文件)。
(4)打开文件后,以后对这个文件的读写操作,都是针对内存中这一份动态文件的,而并不是针对静态文件的。当我们对动态文件进行读写后,此时内存中的动态文件和块设备中的静态文件就不同步了,当我们close关闭动态文件时,close内部内核将内存中的动态文件的内容去更新(同步)块设备中的静态文件

(5)常见的一些现象
第一个:打开一个大文件时比较慢
第二个:我们写了一半的文件,如果没有点保存直接关机/断电,重启后文件内容丢失。

(6)为什么要这么设计
以为块设备本身有读写限制(回忆NnadFlash、SD等块设备的读写特征),本身对块设备进行操作非常不灵活。而内存可以按字节为单位来操作,而且可以随机操作(内存就叫RAM,random),很灵活。所以内核设计文件操作时就这么设计了。

3、文件描述符
(1)文件描述符其实实质是一个数字,这个数字在一个进程中表示一个特定的含义,当我们open打开一个文件时,操作系统在内存中构建了一些数据结构来表示这个动态文件,然后返回给应用程序一个数字作为文件描述符,这个数字就和我们内存中维护这个动态文件的这些数据结构挂钩绑定上了,以后我们应用程序如果要操作这一个动态文件,只需要用这个文件描述符进行区分。
(2)一句话讲清楚文件描述符:文件描述符就是用来区分一个程序打开的多个文件的。
(3)文件描述符的作用域就是当前进程,出了当前进程这个文件描述符就没有意义了


二,简单的文件读写实例

linux常用文件IO接口
open、close、write、read、lseek

1、打开文件与关闭文件
(1)linux中的文件描述符fd的合法范围是0或者一个正正数,不可能是一个负数。
(2)open返回的fd程序必须记录好,以后向这个文件的所有操作都要靠这个fd去对应这个文件,最后关闭文件时也需要fd去指定关闭这个文件。如果在我们关闭文件前fd丢掉了那就惨了,这个文件没法关闭了也没法读写了。

需要注意的是,使用API时要记得包含头文件,一般使用man手册查看原型时,在开始的部分就有要包含的头文件

2、实时查man手册
(1)当我们写应用程序时,很多API原型都不可能记得,所以要实时查询,用man手册
(2)man 1 xx 查linux shell命令,man 2 xxx 查API, man 3 xxx 查库函数

3、读取文件内容
(1)ssize_t read(int fd, void *buf, size_t count);
fd表示要读取哪个文件,fd一般由前面的open返回得到
buf是应用程序自己提供的一段内存缓冲区,用来存储读出的内容
count是我们要读取的字节数
返回值ssize_t类型是linux内核用typedef重定义的一个类型(其实就是int),返回值表示成功读取的字节数。

4.打开文件并读文件内容

#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;		             // fd 就是file descriptor,文件描述符
	char buf[100] = {0};             //输出型数组,把读取的数据填充到这个数组里面
	int ret = -1;
	
	// 第一步:打开文件
	fd = open("a.txt", O_RDWR);
	if (-1 == fd)		               // 有时候也写成: (fd < 0)
	{
		printf("文件打开错误\n");
	}
	else
	{
		printf("文件打开成功,fd = %d.\n", fd);
	}
	
	
	// 第二步:读文件
	ret = read(fd, buf, 10);
	if (ret < 0)
	{
		printf("read失败\n");
	}
	else
	{
		printf("实际读取了%d字节.\n", ret);
		printf("文件内容是:[%s].\n", buf);
	}

	// 第三步:关闭文件
	close(fd);
	
	return 0;
}

执行结果
在这里插入图片描述

5、向文件中写入
(1)写入用write系统调用,write的原型和理解方法和read相似
(2)注意const在buf前面的作用,意为输入性参数。
(3)注意buf的指针类型为void,意为万能匹配类型。

6.打开文件写文件

#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;		             // fd 就是file descriptor,文件描述符
	char writebuf[20] = "l love linux\n";    //输入型参数
	int ret = -1;
	
	// 第一步:打开文件
	fd = open("a.txt", O_RDWR);
	if (-1 == fd)		               // 有时候也写成: (fd < 0)
	{
		printf("文件打开错误\n");
	}
	else
	{
		printf("文件打开成功,fd = %d.\n", fd);
	}
	
	// 第二步:写文件
	ret = write(fd, writebuf, strlen(writebuf));
	if (ret < 0)
	{
		printf("write失败.\n");
	}
	else
	{
		printf("write成功,写入了%d个字符\n", ret);
	}
	

	// 第三步:关闭文件
	close(fd);
	
	return 0;
}

执行结果
在这里插入图片描述


三,open函数的flag

open函数的原型

在这里插入图片描述

1.open函数的常用flag详解1

1、读写权限:O_RDONLY, O_WRONLY, O_RDWR
(1)linux中文件有读写权限,我们在open打开文件时也可以附带一定的权限说明(譬如O_RDONLY就表示以只读方式打开,O_WRONLY表示以只写方式打开,O_RDWR表示以可读可写方式打开
(2)当我们附带了权限后,打开的文件就只能按照这种权限来操作。

使用方法

在这里插入图片描述

2、打开存在并有内容的文件时:O_APPEND、O_TRUNC
(1)思考一个问题:当我们打开一个已经存在并且内部有内容的文件时会怎么样?

可能结果1:新内容会替代原来的内容(原来的内容就不见了,丢了)
可能结果2:新内容添加在前面,原来的内容继续在后面
可能结果3:新内容附加在后面,原来的内容还在前面
可能结果4:不读不写的时候,原来的文件中的内容保持不变

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

2.open函数的常用flag详解2

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

(2)open的flag O_CREAT就是为了应对这种打开一个并不存在的文件的O_CREAT就表示我们当前打开的文件并不存在,我们是要去创建并且打开它。

(4)当我们open使用了O_CREAT,但是文件已经存在的情况下会怎样?经过实验验证发现结果是报错

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

(6)这样可能带来一个问题?我们本来是想去创建一个新文件的,但是把文件名搞错了弄成了一个老文件名,结果老文件就被意外修改了。我们希望的效果是:如果我CREAT要创建的是一个已经存在的名字的文件,则给我报错,不要去创建

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

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

在这里插入图片描述

2、O_NONBLOCK
(1)阻塞与非阻塞。(比如你去银行排队取钱,看到有很多人在排队,你可能有两种做法:第一种,无论前面有多少人,你也跟着排在后面等候,一直等到轮到你取钱,这就相当于阻塞第二种,当你看到有很多人排队,你就先去干其他事情,过一会再过来看,如果看到人还是很多就再次离开,再过一会来看,直到再次来看的时候没人排队了你就去取钱,这就相当于非阻塞)

如果一个函数是阻塞式的,则我们调用这个函数时当前进程有可能被卡住(阻塞住,实质是这个函数内部要完成的事情条件不具备,当前没法做,要等待条件成熟),函数被阻塞住了就不能立刻返回;如果一个函数是非阻塞式的那么我们调用这个函数后一定会立即返回,但是函数有没有完成任务不一定

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

(3)操作系统提供的API和由API封装而成的库函数,有很多本身就是被设计为阻塞式或者非阻塞式的,所以我们应用程度调用这些函数的时候心里得非常清楚。

(4)我们打开一个文件默认就是阻塞式的,如果你希望以非阻塞的方式打开文件,则flag中要加O_NONBLOCK标志
(2)只用于设备文件,而不用于普通文件

3、O_SYNC
(1)write阻塞等待底层完成写入才返回到应用层
(2)无O_SYNC时write只是将内容写入底层缓冲区即可返回,然后底层(操作系统中负责实现open、write这些操作的那些代码,也包含OS中读写硬盘等底层硬件的代码)在合适的时候会将buf中的内容一次性的同步到硬盘中。这种设计是为了提升硬件操作的性能和销量,提升硬件寿命;但是有时候我们希望硬件不好等待,直接将我们的内容写入硬盘中,这时候就可以用O_SYNC标志

4.扩展:exit、_exit、_Exit退出进程
(1)当我们程序在前面步骤操作失败导致后面的操作都没有可能进行下去时,应该在前面的错误监测中结束整个程序,不应该继续让程序运行下去了。
(2)我们如何退出程序?
第一种;在main用return,一般原则是程序正常终止return 0,如果程序异常终止则return -1。
第一种:正式终止进程(程序)应该使用exit或者_exit或者_Exit之一。

在这里插入图片描述


四,文件读写的一些细节

1、errno和perror
(1)errno就是error number,意思就是错误号码。linux系统中对各种常见错误做了个编号,当函数执行错误时,函数会返回一个特定的errno编号来告诉我们这个函数到底哪里错了
(2)errno是由OS来维护的一个全局变量,任何OS内部函数都可以通过设置errno来告诉上层调用者究竟刚才发生了一个什么错误
(3)errno本身实质是一个int类型的数字,每个数字编号对应一种错误。当我们只看errno时只能得到一个错误编号数字(譬如-37),不适应于人看。
(4)linux系统提供了一个函数perror(意思print error),perror函数内部会读取errno并且将这个不好认的数字直接给转成对应的错误信息字符串,然后print打印出来。好的程序风格要懂得使用perror函数,进行错误信息打印。

在这里插入图片描述

2、read和write的count
(1)count和返回值的关系。count参数表示我们想要写或者读的字节数返回值表示实际完成的要写或者读的字节数。实现的有可能等于想要读写的,也有可能小于(说明没完成任务)

在这里插入图片描述

(2)count再和阻塞非阻塞结合起来,就会更加复杂。如果一个函数是阻塞式的,则我们要读取30个,结果暂时只有20个时就会被阻塞住,等待剩余的10个可以读。
(3)有时候我们写正式程序时,我们要读取或者写入的是一个很庞大的文件(譬如文件有2MB),我们不可能把count设置为210241024,而应该去把count设置为一个合适的数字(譬如2048、4096),然后通过多次读取来实现全部读完

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

(2)应用层C语言库函数提供了一些用来做文件读写的函数列表,叫标准IO。标准IO由一系列的C库函数构成(fopen、fclose、fwrite、fread),这些标准IO函数其实是由文件IO封装而来的(fopen内部其实调用的还是open,fwrite内部还是通过write来完成文件写入的)。标准IO加了封装之后主要是为了在应用层添加一个缓冲机制这样我们通过fwrite写入的内容不是直接进入内核中的buf,而是先进入应用层标准IO库自己维护的buf中,然后标准IO库自己根据操作系统单次write的最佳count来选择好的时机来完成write到内核中的buf(内核中的buf再根据硬盘的特性来选择好的实际去最终写入硬盘中)。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值