系统编程--文件IO

系统调用

简介

在这里插入图片描述
系统调用,就是一些操作系统所提供的函数API,通过键入man man,可以查看man手册的简介,可以看到,man手册的第二卷,都是系统调用的函数原型,即内核提供的函数

系统函数与系统调用(小细节)

在这里插入图片描述
实际上,我们所使用的所谓的系统提供的函数,如上图中的open函数,他并不直接就是原生态的系统调用,他是在原生态的系统提供的函数的基础上进行了一层套壳(如函数名,参数等),其内部实现原理不变,也就是进行了浅封装,这是因为操作系统并不希望你直接窥探到其原生态的函数,所以进行一下浅封装。

因此,严格来讲,我们所使用的函数,只能叫做系统函数,而其所对应的被封装的系统提供的函数,才是系统调用,如下图:
在这里插入图片描述

当然,大多数情况下,我们并不细究他们的区别,我们称open函数,即系统调用,这也是可以的,了解其真正的原理即可

理解HelloWorld如何打印到屏幕

在这里插入图片描述
首先,在用户级,运行库函数:printf,表示想要将HelloWorld打印到屏幕上,
之后,该函数去找系统函数:write函数,将该任务交给他,
而write函数,又将该任务交给其真正对应的系统调用sys_write函数(这一步实现了用户层到内核层)
到了内核级,sys_write函数就去寻找相应的驱动,驱动程序驱动硬件,完成HelloWorld的显示

open/close函数

open

man 2

函数原型

在这里插入图片描述
打开or创建一个文件

参数:1、文件路径 2、打开方式
对于第二个参数:
在这里插入图片描述
有三个选项,分别是只读、只写、读写

返回值:
在这里插入图片描述
成功:返回一个文件描述符(文件描述符就是一个整数)
失败:返回-1,并设置errno为恰当的值

补充

1、对于第二个open,其中第三个参数是设置文件的权限,该函数只有在创建文件时,如果想设置权限,可以用到该函数
2、
在这里插入图片描述
对于第二个参数,还有上图中圈出来的这些可以使用

代码(以及跳转快捷键)

小tips:将光标放在函数名上,命令行模式下键入 卷数 + K

打开已存在的文件

在这里插入图片描述
由man手册我们已经知道,open函数需要三个头文件,而前两个可以使用上图的第一个头文件代替

创建一个文件并设置权限

在这里插入图片描述
第二个参数的意思是:如果文件存在,则以只读打开,如果文件不存在,则创建,并设置权限为644
在这里插入图片描述

清空

在这里插入图片描述
第二个参数:如果文件存在,则打开并清空,如果不存在,则创建,并设置权限为644(三个宏,第一个宏和第三个宏是一组,第二个宏是另一组,两种情况)

注意,这里只读与清空是不冲突的,只要那个指定的文件的权限是可写的就可以

第三个参数:权限设置(详细)

在这里插入图片描述
对于权限的数字表示:
三个权限分别代表4、2、1,即二进制的2的2次方、1次方、0次方
其每组权限的值相加,就是该组的权限数,三个组的权限数拼到一起,就是最终的权限数

即,该过程就是:每三个二进制数转为各自转为十进制后,相加生成一个数,该数就是一个八进制数当前位对应的数值。

所以,任意一个权限数,如511,他的权限是r-x --x --x,对应到权限数就是511,而对应到二进制就是101001001,将该二进制转为八进制就是511,所以,权限数的计算,完美契合八进制的转换,所以,我们可以使用任意的三位八进制数来表示权限数

二进制转八进制,与二进制转十进制唯一不同的是,八进制要每三个一组,组内转为十进制后相加,组外直接拼接,(而十进制则是所有都转为十进制之后相加)

命令行键入umask,可以看到显示0002,第一个0表示八进制,后面是其值002(八进制):对应权限000000010
而对umask取反,得到:111111101
并转为八进制,则是775

如果我们设置权限为511:
在这里插入图片描述
775 & 511,rwx rwx r-x & r-x --x --x,最终结果是r-x --x --x,所以最终的结果就是511
(有时会被位运算775影响,有的最终结果还是原来的值,所以最好算一下)

注意,传入时要加上前缀0,该前缀表示接下来是八进制数
在这里插入图片描述

close

原型

在这里插入图片描述

在这里插入图片描述
成功:返回0
失败:返回-1,设置errno

errno和strerror

在这里插入图片描述
errno可以理解为系统的全局变量,所以无需定义,引入一个头文件即可(<errno.h>)
本质上他与环境变量有关

open函数如果出错了,会返回-1,设置errno,errno就是一个全局变量,可以直接打印

如上图,如果打开失败,则函数返回-1,设置errno

但是如果只是打印一个errno,他只会打印一个int型的数,我们并不知道是什么意思,所以可以使用strerrnum,来打印错误信息:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

总结

在这里插入图片描述

read和write函数

read

原型

在这里插入图片描述
参数:
1、哪个文件描述符中读
2、读到哪个缓冲区(一般是字符数组)
3、缓冲区的大小(字符数组所开辟的空间大小)
返回值:
成功:返回读到的字节数(不包括\0,之所以不包括\0,一是因为read本身就不会读\0,二是因为read函数读的数据就是write写过来的,write不会将\0写过来),返回0表示读到文件尾
失败:返回-1,设置errno

总结

在这里插入图片描述

write

函数原型

在这里插入图片描述
注意,第二个参数用const,因为他是待写出的数据缓冲区,也就是将第二个参数的内容写到第一个参数中,为了避免写的过程中误修改,使用const加以限定
第三个参数是缓冲区中的有效数据大小,而不是开辟的数据大小(一般是直接使用每次read函数的返回值,或者strlen(buf))

返回值:
成功:返回写入的字节数,如果返回0,则表示全部写入
失败:返回-1,设置errno

代码

在这里插入图片描述
在这里插入图片描述
可以在main函数中,将参数加上,之后在运行程序时,就可以指定一些参数,俗称,启动参数,他们会从argv数组的下标1位置开始存入,数组元素类型是字符串

perror函数

在这里插入图片描述
函数原型:
在这里插入图片描述
该函数允许自定义打印错误信息,同时,会打印系统报错,即该函数在结合了上面所说的“errno”以及“strerror()”之后,还另外允许我们自定义报错信息

我们应该在上面的代码中,对每个系统调用进行返回值检查

最终效果:
在这里插入图片描述

预读入缓输出

对比库函数和系统调用函数的效率

在这里插入图片描述
首先我们看上面这张图,我们会发现,当使用库函数时,库函数会去先调用系统函数,然后调用驱动,而使用系统函数,则直接进行系统调用驱动,表面上看,是省去了一步,所以系统调用一定比库函数快吗?

实际上正好相反,对于read和fputc两个函数,如果我们都对其设置为一次读一个字节,那么最后测试的结果是:库函数要远远快于系统函数。这是因为,(如下图),库函数每次虽然是读一个字节,但他有自己的缓冲区,只有缓冲区满了,再把整个积攒的数据写到系统,而系统函数则每读到一个字节,就写给系统,来回往复。
而这个从用户级到系统级的穿越,是非常耗费时间的,库函数虽然一次读一个字节,但他内核并不是老老实实的一次向系统写一个字节,而是放到自己的缓冲区,最后统一写过去,这样的话,跨越级别的操作就很少,而如果设置系统函数read一次一个字节,那么他会老老实实的一次写一个字节,每次都进行用户级到系统级的跨越,所以,这个层面来讲,系统函数不如库函数快
在这里插入图片描述

在这里插入图片描述
上面那种缓冲区机制,在系统内核中尤为常见,比如,将系统向磁盘读写数据时,都是用自己的缓冲区,进行速度的优化,我们称之为预读入、缓输出

总结

上述情景告诉我们,对于系统函数和库函数,各有各的优劣,有些情况下,库函数甚至要比系统函数快,所以不要有“系统函数一定比库函数好”这种思想,要择不同的业务需求使用不同的函数

文件描述符

原理

在这里插入图片描述
首先,对于一个可执行程序而言,一个可执行程序就对应着一个进程,该程序的整个内部数据如上图所示,0~3G段,是用户级数据,包括源代码、堆、栈、库、环境变量等等,
而3G~4G区域,是系统级,内核级,在该区域,有一个结构体(叫PCB进程控制块),结构体中有个成员,类型是指针,该指针就指向了文件描述符表

而对于文件描述符表:
在这里插入图片描述
他是一组键值对,key从0开始,对应的value是指针类型的变量,该指针指向文件结构体,文件结构体则表述着一个文件的信息,而文件描述符,就是这些键值对的key,所以,他也是int型,操作系统不希望我们深究value的内容,他将value的内容做了隐藏,所以,我们只能使用前面索引的key,来操作文件,这就是,文件描述符

注意:
1、对于每个可执行程序来说(或者说对于每个进程,因为一个可执行程序对应一个进程),其文件描述符表的0、1、2都被系统占用,分别是标准输入、标准输出、标准出错,所以,新开的文件要从3开始向后排,而如果开了3、4、5、6,而这时我们关闭了3,则再开一个新文件,会占用3的位置,也就是会占可用的最前面的位置
2、而其上限是1024个,也就是默认情况下,一个进程(或一个可执行程序)最多打开1024个文件(当然某些情况下,这是可以修改的)
3、如果我们想要使用0、1、2这些系统文件,最好使用其对应的宏,而不是key

阻塞与非阻塞

前提

在这里插入图片描述
产生阻塞的场景只有两个:
1、读设备文件
2、读网络文件
只有在这两种情景下,才会涉及到阻塞非阻塞,除此之外,没有阻塞的概念,之后我们先利用读设备来研究阻塞非阻塞

而我们电脑的终端设备都放在/dev/tty文件夹中

代码演示1

在这里插入图片描述
我们通过read,从标准输入中读数据,实际上就是从我们输入缓冲区文件中读,他也属于设备文件
在这里插入图片描述
我们启动之后,会发现其阻塞着,等待我们键入“标准读入”的数据

而只有设备文件、网络文件有这种属性。
设备文件、网络文件默认是阻塞
由此可知,我们在win中使用“cin>>”读数据的时候,也是设备文件,也是阻塞

修改设备文件(读入设备)为非阻塞

原理

我们已经知道,标准读入设备文件,默认是打开状态,因为上面的代码中,我们没有使用open,就可以直接read,说明其是打开状态
而想要修改文件属性,可以使用open函数,第二个参数可以设置非阻塞。
理论上我们应该是要先close,之后再open打开,但是实际操作中,我们直接进行open即可

代码

在这里插入图片描述
在这里插入图片描述

返回值

在这里插入图片描述
当返回值是-1,且设置errno为EAGAIN或者EWOULDBLOCK,该返回值表示所读的系统设备(或网络设备)非阻塞,且无数据

代码优化(设置超时)

在这里插入图片描述
在这里插入图片描述
只尝试五次,五次如果都没数据,则关闭了

总结

在这里插入图片描述
思考:设置/dev/tty为非阻塞之后,为什么还能正常使用STDOUT_FILENO,因为标准输出是不是阻塞都无所谓,他的功能只是打印数据,
所以,设置该文件为非阻塞只是将该文件内能够产生阻塞效果的终端文件进行了修改

fcntl函数

使用场景

前面我们设置文件为非阻塞时,文件处于打开状态,还要使用open进行重新打开,来设置文件访问控制属性(阻塞非阻塞),现在使用fcntl函数同样可以设置文件属性,且不用重新打开文件,但是要求文件是打开的(标准输入输出出错等文件默认打开)

原型

在这里插入图片描述
第一个参数是文件描述符,第二个是一个选项,之后是可变参数,根据第二个参数的选项来决定可变参数

代码

在这里插入图片描述
首先第二个参数设置为F_GETFL,拿到初始状态下文件的属性,并判断是否成功(失败返回-1),

之后使用位或的操作,位或的好处时,不管原先是什么,都在原来的基础上,加上一些属性,不受原来属性的影响。所以flags |= O_NONBLOCK,就是在原来属性的基础上,加了非阻塞属性,

之后使用F_SETFL参数,将新的属性设置回去,并检查返回值

接下来的代码与之前使用open设置非阻塞的代码一样了
在这里插入图片描述

关于flags

在这里插入图片描述
flags,是一个int型的数据,而实际上他的底层是一个位图,位图就是很多个二进制位,int型变量有32个二进制位,其中每一个位是0还是1,表示着某个属性是否被设置,如果是0,表示没生效,1,则表示生效

所以,代码中:
int flags = fcntl(STDIN_FILENO,F_GETFL);
是拿到了初始状态的位图

之后flags |= O_NONBLOCK
是在初始状态的基础上,无视初始状态的影响,将O_NONBLOCK位置上设置为1,即将其生效,并更新到flags变量上。

最后再次使用fcntl函数,将新的状态设置回去

lseek函数

函数原型

在这里插入图片描述

在这里插入图片描述
作用:设置一个文件读写的光标位置。或者说改变文件的读写位置,或者说改变文件的偏移量

参数3:指定起始偏移位置,也就是以哪个位置为基准,对光标进行移动(或者叫对光标进行偏移)
有三个宏:如上图所示
分别代表:
文件最初的位置
文件当前的光标位置
文件的末尾
参数2:光标的偏移量,也就是以参数3为基准偏移几个字节,且该参数是一个矢量,可以指定向左还是向右

参数1:就是我们所操作的文件了

注意返回值:成功的话,返回“读写位置”的最终位置,相对于“起始位置”的偏移量

应用场景(+知识补充)

场景一:文件偏移

在这里插入图片描述
文件在打开时,除了以O_APPEND的方式打开之外,其他方式打开,其读写位置都是0。

但是,如果在一个打开了的文件中,读操作和写操作使用的“读写位置”是同一个,也就是他们任何一方操作完之后,光标移动完之后,只要文件没有被关闭,其光标的位置不会变

测试

代码:
在这里插入图片描述
结果:
在这里插入图片描述

可以看到没有任何的输出,也就是在write之后,再去执行读,是没有读到任何的内容的,这就是因为文件的“读写位置”在wirte之后,来到了最末尾,从这里开始读,什么也没有,肯定read返回的是0,所以,就没有读到任何内容
如下图图解:
在这里插入图片描述

解决:
在这里插入图片描述
为了解决,我们就可以使用lseek函数对文件的“读写位置”进行新的设置,如上图:我们设置“读写位置”来到“从SEEK_SET开始,偏移0字节”,也就是来到了文件的起始位置,这样就可以对刚刚写入的内容进行读取了

场景二:获取文件大小

在这里插入图片描述
我们可以打开一个文件之后,直接将“读写位置”移动到文件末尾,由于lseek函数会返回移动之后的“读写位置”相对于“文件开头”的偏移量(单位字节),所以可以由此得到文件的大小

代码如上图所示,首先从命令行获取参数argv[1]:表示输入命令行的第二个字符串,而由于第一个字符串是命令名,而参数紧随其后,所以第二个字符串就是第一个参数了

之后打开文件,调整“读写位置”,其返回值就是文件的大小

文件末尾,就是指向了文件中最新的可用位置,将其下标 - 初始下标,结果正好是文件长度:
在这里插入图片描述
3 - 0 = 3

而至于文件末尾有没有‘\0’,这个不是文件的特性,而是根据写入的字符串具体分析的,而除了以当个字符当元素,且没有‘\0’的字符数组,其他都有\0,除此之外,同时还要考虑write写入了几个字节,总是要看是否写入了\0

场景三:拓展文件大小

在这里插入图片描述
假如说我们想要拓展文件,拓展111个字节,

首先我们使用lseek,将“读写位置”从文件末尾,往后偏移110个字节,这里要留出一个字节不偏移

之后,要注意,如果想拓展文件大小,必须要引起IO操作,所有那一个字节就是留着做IO操作用的,当移动完之后,写入一个字符(一字节),这样就完成了文件大小的拓展

在这里插入图片描述
可以看到虽然打印了887,那是因为打印的时候没包括最后的‘a’,实际上大小已经被拓展为了888

我们打开文件看一下:
在这里插入图片描述
中间没有写入字符的地方,称之为“文件空洞”,他们实际上就是‘\0’

所以我们也可以在操作IO时,将‘a’替换成’\0’

原理补充:
在这里插入图片描述
从下标3 到 下标4,是lseek偏移的过程

之后向下标4写入一个字符,其“读写位置”(光标)自动向下移动一位,也就到了下标5

此时,文件大小变为了5 - 0 = 5 ,拓展了2字节,即这2字节是lseek偏移量 + 最后IO的一字节

补充:拓展文件大小(正规军写法)

在这里插入图片描述
一般是使用truncate函数去拓展文件大小

参数一:文件路径(精确到文件名),这个路径是相对于当前.c文件的路径,且该文件必须存在,不可由系统创建

参数二:重新设置文件的大小

总结

在这里插入图片描述

传入、传出、传入传出参数

在这里插入图片描述
需要注意的是:
1、对于传入参数,最好用const来限定

2、对于传出参数,要让其指向的空间有

  • 8
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值