目录
Linux文件概述
系统IO
创建文件creat()
打开文件open()
写文件write()
读文件read()
文件指针---lseek()
系统IO拷贝
标准IO
标准IO和系统IO的区别
缓冲区的分类
行缓存测试
打开文件fopen()
写文件fwrite()
读文件read()
标准IO拷贝
标准IO和系统IO的效率
mmap内存映射
上节我们学习了Linux操作系统概述,本节开始学习Linux文件编程!
Linux文件概述
Linux一点哲学:一切皆为文件file
在Linux中对目录和设备的操作都等同于对文件的操作,都是使用文件描述符来进行的。
(一切都可以抽象概括为文件,包括硬件,比如在电脑上插上U盘,操作U盘其实就是在操作这个文件)
Linux文件可分为:普通文件,目录文件(文件夹),链接文件(软链接,可以指向特定的文件,类似windows上的快捷方式),设备文件(硬件设备,比如U盘);
1、当打开一个现存文件或创建一个新文件时,内核就向进程返回一个文件描述符;当需要读写文件时,也需要把文件描述符作为参数传递给相应的函数。
2、文件描述符是一个非负的整数(从数字0开始,每打开一个文件,这个文件描述符就加1,它其实指向的是内核中的文件属性),它是一个索引值,并指向在内核中每个进程打开文件的记录表。
3、一个进程启动时,都会打开3个文件:标准输入、标准输出和标准出错处理 (这三个文件的描述符是0 1 2,所以我们用户使用的文件是从3开始的)。
进程默认的文件描述符:
系统IO
Linux操作系统给用户提供一些操作文件(文件夹)的操作,需要掌握的有:
open //打开文件
read //读取文件
write //写入文件
close //关闭文件
fseek //修改偏移量
注:这几个函数只能在Linux系统运行,不能在Windows里面运行。
接下来在虚拟机的Linux系统上试一下创建文件这个操作
创建文件creat()
先创建一个文件夹Linux
然后再在这个文件夹下创建一个文件存放今天所学的内容
然后在这个目录下创建一个.c文件
框架都是和C一样的,只是函数不一样
接下来我们要知道使用creat()这个函数需要包含什么头文件
Linux给我们提供了一个非常重要的东西,叫man手册
之前我们C语言库函数不知道包含什么头文件的时候要查询时,输入的是man 3 函数名,总之我们在Linux不知道用什么头文件的时候或者要查看函数的说明的时候都可以用man函数来查询,它后面要跟的数字可以是0~9,其中常用的是1 2 3,也可以不跟数字。
补充命令14
man 1 命令:表示要查找的是命令;
man 2 函数名(系统调用):表示要查找的是系统调用;
man 3 函数名(库函数):表示要查找的是库函数;
我们可以输入:man 2 函数名就可以查询它要包含的头文件
输入命令回车后就可以看到函数原型和要包含的头文件了,直接复制这三个头文件
补充命令15:在查看man手册的界面中,如果翻到页面底下想要回到开头,就输入:gg
这个函数有两个参数:
第一个参数是文件的路径pathname(包含文件名,如果不写路径只写文件名则默认创建在当前目录下面),
第二个参数mode是指定文件的权限
权限可以照着手册中的宏定义写,可以写左边英文字符那一列,也可以写中间数字那一列,后面是权限的描述。
比如我们要创建一个文件名叫hello.c的文件,默认放在当前目录下,然后指定用户权限时可读可写
两个权限之间用一个”|”符号或起来。
然后这个函数的返回值是返回一个新的文件描述符或者如果有错误发生的话就返回-1
那么我们可以对这个返回做一下判断,如果出错就打印错误原因
在linux中,出现错误的原因一共有144种,perror这个函数就可以把错误的原因打印出来
运行结果是3
并且它真的在当前目录下创建了一个hello.c的文件
它的权限是当前用户可读r可写w
我们可以随便写一个不存在的路径,让perror函数提示错误看看
运行后它给出提示没有这个文件或者目录
打开文件open()
打开文件使用的是open()这个函数
通过man手册可以看到这个函数有两种形式
这两个有什么区别呢?
如果是打开一个已经存在的文件就用上面那个只有两个参数的形式,如果是这个文件不存在,要先创建这个文件,然后再打开就使用有三个参数的形式。
下面创建一个2.open.c的文件开始用代码实现这个操作
首先是打开一个已经存在的文件
第一个参数是路径和文件名pathname,第二个参数flag是打开的方式(即打开的这个文件的目的是写还是读?还是又读又写?),一共有三种打开的方式:只读RDONLY,只写WRONLY,读写RDWR
返回值正常是返回一个新的文件描述符,不正常是返回-1
打开文件后要记得关闭文件,关闭文件用close()函数来实现,close()的头文件是
直接把文件描述符传给它,一旦文件打开,接下来的操作都是文件描述符。
运行结果只是打开关闭的操作,没有什么现象
如果这时我们删除掉hello.c这个文件,再运行打开操作就会提示我们错误
第二种形式创建并打开文件
如果打开之前要创建的话需要或上这个参数
运行后打开和关闭操作没有现象显示出来,但是发现已经产生了一个新的文件
这时我们再次运行这个程序它也并没有给我们提示
但是我们希望的是如果这个hello.txt已经存在了,如果我们再次运行这个程序的话它可以给我们提示,因此我们可以在后面再或上这个参数
这个参数的作用就是如果文件存在则报错
加上后,再运行就可以看它给我们警告说文件已存在
写文件write()
创建一个3.read.c的文件完成读写文件的操作
写操作用write()函数来实现,要包含这个头文件
它有三个参数,第一个参数是文件描述符fd,也就是要往哪个文件中写,第二个文件是一个指针buf保存要写的那个东西的地址,也就是要写什么东西,第三个是count表示要写多少个字节
返回值是如果成功就返回写入字节的个数,否则返回-1
运行后打开hello.c就看到我们写入的”hellloworld”
接下来开始读的操作
读文件read()
读用的是read()函数完成,包含头文件
它有三个参数,第一个参数是文件描述符(从哪个文件读),第二个是指针(读到哪,指针存放了要读到哪里的地址),第三个是读多少个字节
返回值是如果成功就返回写入的字节数,失败返回-1
运行后,没有打印什么东西出来,
为什么?
是因为文件都有一个文件指针,
我们一开始每往里面写一个字节,指针就往后移动一下,当我们写到最后一个字节的时候,指针指向了空的位置,所以我们最后读出来的时候没有读到东西。
怎么办?
我们可以在读之前把这个文件指针挪回来指向第一个字节的位置。
怎么挪这个文件指针?
我们需要用到lseek()这个函数
它有三个参数:fd移动哪个文件的指针,offset移动多少个字节(正数表示向后移动,负数表示向前移动),显然我们是要向前移动,whence相对谁来移动,相对位置有三个, SEEK_SET相对文件开头,SEEK_CUR相对当前位置,SEEK_END相对文件末尾
这样就读出来了
移动文件指针还有另外两种写法
这样都能实现效果
接下来具体讲讲这个文件指针
文件指针---lseek()
lseek()的返回值什么?
刚刚我们用这个函数来讲文件指针挪到了文件开头
很显然这个函数的返回值是0,返回的是文件指针距离文件开头的字节数,文件开头是0个字节
因此lseek()的返回值是文件指针距离文件开头的字节数。
我们如何用lseek()求文件的长度?
打开一个文件,将文件指针移动到文件的末尾,返回的就是文件的长度,而且是以字节为单位的。
注意,它的返回值是off_t类型,这个类型等会儿要printf怎么打印出来?
Off_t其实是long_int类型,所以占位符应该用%ld
下面用命令行参数来实现一下
如果已经忘记命令行参数了,可以先去复习一下这篇博客
嵌入式全栈开发学习笔记---C语言笔试复习大全20-CSDN博客
创建文件4.lssek.c
运行结果
我们在运行命令的后面加上要计算文件长度的文件回车就能看到计算出来的长度了
如果想要知道计算出来的对不对,可以查看文件属性对照一下
系统IO拷贝
cp 文件1 文件2实现的效果是将文件1中的东西拷贝到文件2
从文件读写的角度来看它怎么实现的呢?
它是先创建x.c这个文件,然后打开x.c和1.creat.c,将1.creat.c中的东西不停地读到x.c文件中,操作完成后就把这两个文件关闭就行了
那读到什么时候结束呢?我们知道read()这个函数的返回值是读到的字节数,当它返回值是0时就表示没有东西读了,所以读到的字节数0就表示结束了。
下面我们就用以上学过的操作来模拟这个拷贝的过程
创建文件5.copy.c
运行结果
打开x.c就可以看到里面的内容和2.open.c一模一样
补充1:return 0, -1,-2,-3......返回的数字不一样会有什么实际的效果?
这里需要补充一个命令:
补充命令16:echo $?
这个命令可以查看程序return回来的是什么
如果把正常结束的程序这里改成return 100,最后可以看到返回来的是100
所以我们return不同数字的好处就是能查看到底是哪里导致的程序退出
补充2:size_t和ssize_t的区别
size_t是有符号的整型,ssize_t是无符号的整型,详细解释请看这篇博文:
2.详解size_t与ssize_t-CSDN博客
标准IO
标准IO的标准就是标准库的意思,刚刚我们所用的函数都是Linux上的系统调用函数,接下来标准IO操作所用的函数都是C语言给我们提供的函数,具有跨平台的特点。
C库函数的文件操作是独立于具体的操作系统平台的,不管是在DOS、Windows、Linux还是在VxWorks中都是这些函数。
在介绍标准IO之前我们先了解一下数据的两种存储介质和系统IO:
数据的两种存储介质:内存和硬盘
我们来看一下数据的两种存储介质:内存和硬盘
标准IO和系统IO的区别
系统IO
我们刚刚用write这个函数来写操作是跨越性的,它把内存中的东西写到了硬盘上的文件里边。
硬盘上的这个文件就是像我们刚刚创建的hello.c和hello.txt这类普通文件
内存里面的就是像我们刚刚定义的一个字符串数组”helloworld”
在这里,write()进行写操作的时候,这个过程是要进行IO操作的,这个过程就称为“系统IO”
IO操作有点费时间。像我们之前写程序比如变量之间的赋值都是在内存中完成的,速度比较快,但是一旦要跨越介质速度就变慢了,这跟硬盘的物理特性有一定的关系。
但是C语言为我们解决了跨越介质速度慢的问题,它可以减少IO操作的次数,从而提高速度。
标准IO
C语言在内存和硬盘之间加了一个缓冲区,这个缓冲区属于内存,也就是说它其他是内存中挖了一块内存出来,取名字叫缓冲区
每次我们写数据的时候,它其实是“假装”把数据写在这个缓冲区里面,从内存到内存写数据的效率是很高的,等程序写完了,最后再调用write函数将所有数据一次性从缓冲区写入硬盘文件,这样程序运行的效率就比之前直接一个一个地写入硬盘的效率高了,在这里这个过程叫“标准IO”
这种方式本质上是通过减少IO的次数来提高程序运行的效率。
缓冲区的分类
不带缓存的I/O对是文件描述符操作(比如open和write),带缓存的I/O是针对流的。
标准I/O库就是带缓存的I/O,它由ANSI C标准说明。当然,标准I/O最终都会调用上面的I/O例程(也就是标准IO最终还是会调用write()这个函数将数据写入硬盘)。
标准I/O库代替用户处理很多细节,比如缓存分配、以优化长度执行I/O等。
标准I/O提供缓存的目的就是减少调用read和write的次数,它对每个I/O流自动进行缓存管理(标准I/O函数通常调用malloc来分配缓存)。
它提供了三种类型的缓存:
1) 全缓存。当填满标准I/O缓存后才执行I/O操作。磁盘上的文件通常是全缓存的。(写满之后才刷新缓冲区的内容)
2) 行缓存。当输入输出遇到新行符(换行符)或缓存满时,才由标准I/O库执行实际I/O操作。stdin、stdout通常是行缓存的。
3) 无缓存。相当于read、write了。stderr通常是无缓存的,因为要求一旦有错误它必须尽快输出,让用户能立马看到错误提示。
下面我们就用代码来实现来测试一下行缓存
行缓存测试
创建文件test.c
我们写这样一段代码
也许大家会想这段代码一运行就能看到打印出来一个数字,然后等1s后再打印出来第二个数字,之后每次都是等待1s后就输出一个数字,知道循环结束。
但是实际现象是运行之后并没有立马看到打印出来什么东西,而是等了几秒之后就看到一串数字同时出现
我们看到的这个实际现象就是前面所说的行缓冲,当输出0的时候,它是先把0放在缓冲区里面,直到到最后循环结束了,才一下子把数据从缓冲区输出到硬盘文件中,我们在屏幕上才能看到。
如果我们在这段代码中加上一个换行符就不一样了
这样运行的结果才是我们刚开始想的那种效果,代码一运行就能看到打印出来一个数字,然后等1s后再打印出来第二个数字,之后每次都是等待1s后就输出一个数字,知道循环结束
所以我们调试段错误的时候,我们用printf来调试时后面一定要跟上换行符,如果不跟的话程序异常退出就没有刷新,程序正常退出才会刷新(刷新就是进缓冲区的数据输出到硬盘,然后更新缓冲区,只有进行这个刷新过程我们才能在屏幕中看到打印的内容)
打开文件fopen()
函数原型:
FILE *fopen(const char *path, const char *mode);
参数:
path:打开的文件路径;
mode:打开方式。
查标准库函数的说明使用的是man 3
根据man手册提示,这个函数是标准库函数,所以只需要这一个头文件即可
第二个参数mode的取值是:
r:以只读方式打开一个已存在的文件
r+:以可读可写的方式打开一个已存在的文件
w:以只读方式创建并且打开文件(如果文件已存在会把原来的已存在的文件覆盖掉重新创建并打开)
w+:以读写的方式创建并且打开文件(如果文件已存在会把原来的已存在的文件覆盖掉重新创建并打开)
a:以往后追加的方式打开文件,就是原来文件里面有很多东西,一打开文件,文件指针就指向了文件的末尾,然后我们用fwrite写数据的话就是往这个文章的末尾继续追写。如果这个文件不存在就创建这个文件。
a+:和a相同,但是它既能写也能读
返回值是成功返回一个地址(这个地址就是将硬盘里指定的文件跟缓冲区关联起来,接下来我们往缓冲区里写数据其实就是相当于在往我们指定的硬盘文件里面写数据,所以返回的是缓冲区里面的这个地址),否则返回NULL
我们创建一个6.fopen.c文件写代码来测试一下
注:打开文件操作之后调用fclose函数关闭
正常运行后没有报错即可
写文件fwrite()
创建一个7.fread.c的文件测试一下读写的操作
读之前我们首先进行写操作
写操作用fwrite()函数完成
fwrite的头文件和原型,它有四个参数:
第一个参数是一个指针(写什么东西),第二个参数是一块有多大(一般写一块就一个字节,直接写1就行),第三个参数是往里面写多少块,第四个参数是往哪写
它的返回值是size_t类型,返回的是实际写入的字节数,如果异常返回0
运行结果:
Helloworld.txt里面的确有我们写入的helloworld
接着我们要把这个数据读出来
读文件read()
读的操作用fread()函数完成
注意:我们刚刚写完之后,这个文件指针指向了文件的末尾,我们读之前要把它先挪回来,这里移动文件指针使用的不是我们之前用的lseek了,而是用fseek
lseek和fseek的使用差不多,唯一的区别就是返回值,lseek的返回值是移动好的文件指针距离开头的字节数,fseek的返回值就是成功返回0,失败返回-1
之前我们用lseek把文件指针移到文件末尾可以直接求文件长度,而fseek要求文件长度的话需要配合ftell这个函数,也就是先通过fseek函数将文件指针移到文件的末尾,然后通过ftell这个函数获得文件指针距离文件开头的字节数。
运行结果:
如果我们换成“a+”的方式打开,这样的结果就是hello.txt这个文件第一次打开是一个helloworld,第二次是两个helloworld......每次都在后面追加一个helloworld
最后追到到128个字节这个数组就放不下了
标准IO拷贝
接下来我们就用刚刚学习的几个函数和命令行参数模拟一下cp拷贝这个命令
创建一个文件8.copy.c
注:memset()函数的作用是对较大的结构体或数组进行清零操作,需要包含头文件string.h,因为它是字符串处理函数
运行拷贝完毕后
打开x.c文件就可以看到里面的内容和1.creat.c的内容一样了
接下来我们对比一下标准IO和系统IO的效率
标准IO和系统IO的效率
首先我们自己造一个大文件
补充命令17:dd if=/dev/zero of=testfile bs=1M count=256
dd和cp都是拷贝,cp拷贝的是文件,dd拷贝的是数据。
if的意思是input,f就是file,if就是输入文件是/dev/zero,o就是output,of就是输出文件是testfile。也就是说dd可以从/dev/zero文件中源源不断地获取数据0然后写入testfile文件。然后dd也是以块的形式操作的,一块bs等于1M,一共拷贝count=256块。
这样就能很快生成一个大文件testfile
补充命令18:du -sm 文件名
这行命令可以查看文件的大小,du是显示磁盘使用情况,s是summarize显示目录的总大小,m是显示MB中的磁盘使用情况
大文件创建好后,接下来我们来对比一下系统IO的拷贝和标准IO的拷贝的效率
补充命令19:time ./运行具有拷贝功能文件 文件1 文件2
这行命令可以查看运行后将文件1的内容拷贝到文件2中需要多长时间
系统IO拷贝操作所用时间
注意:这个拷贝操作完成后我们先不要着急测试8.copy.c那个标准IO操作,因为我们拷贝操作完testfile后,系统会将testfile加载到内存里面(为了提高下次操作的效率),我们可能需要去清楚一下testfile这个文件再去测试8.copy.c那个标准IO操作,但是有些系统并没有在第一个操作testfile这个文件后就把它加载到缓冲区,这种情况下就不用清除。
如果两次拷贝的效率差不多,说明系统并没有在第一个操作testfile这个文件后就把它加载到缓冲区,这种情况下就不用清除,直接测试标准IO的拷贝效率。
接下来看看标准IO的效率
很明星标准IO的效率比系统IO的拷贝效率高很多。
mmap内存映射
Linux系统:“凭什么我们的系统IO还没有标准IO的效率高?”
Linux系统表示不服气,于是也给我们提供了一种大大提高了效率的文件读写的操作,这个操作要用到系统调用函数mmap(memory mapping)mapping是映射的意思,是一种内存的映射。也就是说我们完全可以可以不借用C语言中的接口(fopen,fwrite等等),同样可以提高读写效率。
怎么操作?
就是真正读写操作之前,提前将磁盘里的文件映射到内存中,相当于投影过来
接下来的操作就是只要操作映射在内存中的这块空间就行了,然后最终它都会把数据保存在磁盘的文件中。这种操作的实质也是为了减少IO操作的次数。
mmap的函数原型和头文件:
它一共6个参数,第一个参数是指定映射在内存中哪块地方,但是如果你对内存的空间不是很有把握的话,就不要指定了,直接写成NULL空,系统随机给我们分配一块空闲的内存就行。
第二个参数是映射的长度,文件多长就映射多长,所以我们首先要获取文件的长度,文件长度属于文件的属性,打开一个文件的时候,这个文件的属性都放在了一个结构体里面了,我们只要获取这个结构体就行了,可以用stat这个系统调用函数获取。
Stat有两个参数,第一个参数是文件名,第二个参数是结构体指针,指向了文件属性的结构体,然后结构体里面有一个成员表示文件的长度。
Stat的返回值成功是0,失败是-1
mmap的第三个参数prot是映射的保护属性/内存属性,比如可以是PROT_READ,也就是映射过来是为了读它。
第四个参数flags,一般选择MAP_SHARED,多个进程可以共享的意思,可以理解为当我们修改了映射在内存里的这块空间数据的时候,磁盘里的文件也随之被修改,也就是其他进程是可以看到这个更新。
第五个参数是fd,文件描述符,即要映射的文件
最后第六个参数offset偏移量,大文件要注意偏移量,如果文件超过1G,一般都需要分多次映射,多次映射就要注意偏移量,小文件就不用偏移了,直接写0
mmap的返回值是如果成功,返回的也是一个地址,我们把磁盘中的一个文件映射到内存中,映射到哪里需要一个地址,我们选择NULL让系统自己分配一块内存,映射完就把这块内存的地址返回,如果失败返回void*类型的-1(MAP_FAILED)
最后用完之后是用munmap来解除映射
代码演示:
创建9.mmap.c来通过mmap来读取文件内容
运行结果就是把8.copy.c文件里面的所有东西读取出来了
如果以后要操作大文件,并且不想使用标准IO的话,就可以用这种方法,这种方法的操作效率比系统IO的效率高很多,操作大文件时可以使用(面试的时候可能会关注操作的效率问题)。
下节开始学习进程控制!
本篇就到这里,下篇继续!欢迎点击下方订阅本专栏↓↓↓